packages feed

haskoin-store 1.8.1 → 1.9.0

raw patch · 76 files changed

+37368/−8170 lines, 76 filesdep +case-insensitivedep +file-embeddep +processdep −scottydep ~QuickCheckdep ~aesondep ~aeson-prettysetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: case-insensitive, file-embed, process, wai-extra, warp-tls

Dependencies removed: scotty

Dependency ranges changed: QuickCheck, aeson, aeson-pretty, base, base16, base64, bytes, bytestring, cereal, conduit, containers, data-default, deepseq, fast-logger, filepath, hashable, hashtables, haskoin-core, haskoin-node, haskoin-store-data, hedis, hspec, http-types, lens, monad-control, mtl, network, nqe, optparse-applicative, random, rocksdb-haskell-jprupp, rocksdb-query, statsd-rupp, stm, string-conversions, text, time, transformers, unliftio, unordered-containers, vault, vector, wai, wai-websockets, warp, websockets, wreq

API changes (from Hackage documentation)

- Haskoin.Store: BadRequest :: Except
- Haskoin.Store: Balance :: !Address -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> Balance
- Haskoin.Store: BinfoAddr :: !Address -> BinfoAddr
- Haskoin.Store: BinfoAddrBalance :: !Address -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> BinfoBalance
- Haskoin.Store: BinfoBlock :: !BlockHash -> !Word32 -> !BlockHash -> !Hash256 -> !Word32 -> !Word32 -> ![BlockHash] -> !Word64 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Bool -> !Word32 -> !Word32 -> ![BinfoTx] -> BinfoBlock
- Haskoin.Store: BinfoBlockHash :: !BlockHash -> BinfoBlockId
- Haskoin.Store: BinfoBlockIndex :: !Word32 -> BinfoBlockId
- Haskoin.Store: BinfoBlockInfo :: !BlockHash -> !BlockHeight -> !Word32 -> !BlockHeight -> BinfoBlockInfo
- Haskoin.Store: BinfoBlockInfos :: [BinfoBlockInfo] -> BinfoBlockInfos
- Haskoin.Store: BinfoDate :: Word64 -> BinfoDate
- Haskoin.Store: BinfoFilterAll :: BinfoFilter
- Haskoin.Store: BinfoFilterConfirmed :: BinfoFilter
- Haskoin.Store: BinfoFilterMempool :: BinfoFilter
- Haskoin.Store: BinfoFilterMoved :: BinfoFilter
- Haskoin.Store: BinfoFilterReceived :: BinfoFilter
- Haskoin.Store: BinfoFilterSent :: BinfoFilter
- Haskoin.Store: BinfoHeader :: !BlockHash -> !Timestamp -> !Word32 -> !BlockHeight -> ![BinfoTxId] -> BinfoHeader
- Haskoin.Store: BinfoHistory :: !Text -> !Text -> !Text -> !Double -> !Double -> !Double -> !Double -> !TxHash -> !Double -> BinfoHistory
- Haskoin.Store: BinfoInfo :: !Word32 -> !Double -> !BinfoSymbol -> !BinfoSymbol -> !BinfoBlockInfo -> BinfoInfo
- Haskoin.Store: BinfoMempool :: [BinfoTx] -> BinfoMempool
- Haskoin.Store: BinfoMultiAddr :: ![BinfoBalance] -> !BinfoWallet -> ![BinfoTx] -> !BinfoInfo -> !Bool -> !Bool -> BinfoMultiAddr
- Haskoin.Store: BinfoRate :: !Word64 -> !Double -> !Double -> BinfoRate
- Haskoin.Store: BinfoRawAddr :: !BinfoAddr -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Int64 -> ![BinfoTx] -> BinfoRawAddr
- Haskoin.Store: BinfoShortBal :: !Word64 -> !Word64 -> !Word64 -> BinfoShortBal
- Haskoin.Store: BinfoSpender :: !BinfoTxId -> !Word32 -> BinfoSpender
- Haskoin.Store: BinfoSymbol :: !Text -> !Text -> !Text -> !Double -> !Bool -> !Bool -> BinfoSymbol
- Haskoin.Store: BinfoTicker :: !Double -> !Double -> !Double -> !Double -> !Text -> BinfoTicker
- Haskoin.Store: BinfoTx :: !TxHash -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word64 -> !ByteString -> !Word32 -> !BinfoTxId -> !Bool -> !Bool -> !Maybe (Int64, Int64) -> !Word64 -> !Maybe Word32 -> !Maybe Word32 -> ![BinfoTxInput] -> ![BinfoTxOutput] -> BinfoTx
- Haskoin.Store: BinfoTxIdHash :: !TxHash -> BinfoTxId
- Haskoin.Store: BinfoTxIdIndex :: !Word64 -> BinfoTxId
- Haskoin.Store: BinfoTxInput :: !Word32 -> !ByteString -> !ByteString -> !Word32 -> !BinfoTxOutput -> BinfoTxInput
- Haskoin.Store: BinfoTxOutput :: !Int -> !Bool -> !Word64 -> !Word32 -> !BinfoTxId -> !ByteString -> ![BinfoSpender] -> !Maybe Address -> !Maybe BinfoXPubPath -> BinfoTxOutput
- Haskoin.Store: BinfoUnspent :: !TxHash -> !Word32 -> !ByteString -> !Word64 -> !Int32 -> !BinfoTxId -> !Maybe BinfoXPubPath -> BinfoUnspent
- Haskoin.Store: BinfoUnspents :: [BinfoUnspent] -> BinfoUnspents
- Haskoin.Store: BinfoWallet :: !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> BinfoWallet
- Haskoin.Store: BinfoXPubBalance :: !XPubKey -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word32 -> !Word32 -> BinfoBalance
- Haskoin.Store: BinfoXPubPath :: !XPubKey -> !SoftPath -> BinfoXPubPath
- Haskoin.Store: BinfoXpub :: !XPubKey -> BinfoAddr
- Haskoin.Store: BlockData :: !BlockHeight -> !Bool -> !Integer -> !BlockHeader -> !Word32 -> !Word32 -> ![TxHash] -> !Word64 -> !Word64 -> !Word64 -> BlockData
- Haskoin.Store: BlockHealth :: !BlockHeight -> !BlockHeight -> !Int32 -> BlockHealth
- Haskoin.Store: BlockRef :: !BlockHeight -> !Word32 -> BlockRef
- Haskoin.Store: CountHealth :: !Int64 -> !Int64 -> CountHealth
- Haskoin.Store: DeriveNormal :: DeriveType
- Haskoin.Store: DeriveP2SH :: DeriveType
- Haskoin.Store: DeriveP2WPKH :: DeriveType
- Haskoin.Store: EventBlock :: !BlockHash -> Event
- Haskoin.Store: EventTx :: !TxHash -> Event
- Haskoin.Store: GenericResult :: a -> GenericResult a
- Haskoin.Store: HealthCheck :: !BlockHealth -> !TimeHealth -> !TimeHealth -> !MaxHealth -> !CountHealth -> !String -> !String -> !Word64 -> HealthCheck
- Haskoin.Store: LogicError :: !String -> CacheError
- Haskoin.Store: MaxHealth :: !Int64 -> !Int64 -> MaxHealth
- Haskoin.Store: MemRef :: !UnixTime -> BlockRef
- Haskoin.Store: PeerInfo :: !ByteString -> !String -> !Int32 -> !Word64 -> !Bool -> PeerInfo
- Haskoin.Store: Prev :: !ByteString -> !Word64 -> Prev
- Haskoin.Store: RawResult :: a -> RawResult a
- Haskoin.Store: RawResultList :: [a] -> RawResultList a
- Haskoin.Store: RedisError :: Reply -> CacheError
- Haskoin.Store: RedisTxError :: !String -> CacheError
- Haskoin.Store: RequestTooLarge :: Except
- Haskoin.Store: SerialList :: [a] -> SerialList a
- Haskoin.Store: ServerError :: Except
- Haskoin.Store: ServerTimeout :: Except
- Haskoin.Store: Spender :: !TxHash -> !Word32 -> Spender
- Haskoin.Store: StoreCoinbase :: !OutPoint -> !Word32 -> !ByteString -> !WitnessStack -> StoreInput
- Haskoin.Store: StoreInput :: !OutPoint -> !Word32 -> !ByteString -> !ByteString -> !Word64 -> !WitnessStack -> !Maybe Address -> StoreInput
- Haskoin.Store: StoreOutput :: !Word64 -> !ByteString -> !Maybe Spender -> !Maybe Address -> StoreOutput
- Haskoin.Store: StringError :: !String -> Except
- Haskoin.Store: ThingNotFound :: Except
- Haskoin.Store: TimeHealth :: !Int64 -> !Int64 -> TimeHealth
- Haskoin.Store: Transaction :: !BlockRef -> !Word32 -> !Word32 -> ![StoreInput] -> ![StoreOutput] -> !Bool -> !Bool -> !Word64 -> !TxHash -> !Word32 -> !Word32 -> !Word64 -> Transaction
- Haskoin.Store: TxData :: !BlockRef -> !Tx -> !IntMap Prev -> !Bool -> !Bool -> !Word64 -> !IntMap Spender -> TxData
- Haskoin.Store: TxId :: TxHash -> TxId
- Haskoin.Store: TxIndexConflict :: ![TxHash] -> Except
- Haskoin.Store: TxRef :: !BlockRef -> !TxHash -> TxRef
- Haskoin.Store: Unspent :: !BlockRef -> !OutPoint -> !Word64 -> !ByteString -> !Maybe Address -> Unspent
- Haskoin.Store: UserError :: !String -> Except
- Haskoin.Store: XPubBal :: ![KeyIndex] -> !Balance -> XPubBal
- Haskoin.Store: XPubSpec :: !XPubKey -> !DeriveType -> XPubSpec
- Haskoin.Store: XPubSummary :: !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word32 -> !Word32 -> XPubSummary
- Haskoin.Store: XPubUnspent :: !Unspent -> ![KeyIndex] -> XPubUnspent
- Haskoin.Store: [address] :: Unspent -> !Maybe Address
- Haskoin.Store: [addresses] :: BinfoMultiAddr -> ![BinfoBalance]
- Haskoin.Store: [after] :: BinfoSymbol -> !Bool
- Haskoin.Store: [age] :: TimeHealth -> !Int64
- Haskoin.Store: [amount] :: BinfoHistory -> !Double
- Haskoin.Store: [balance] :: XPubBal -> !Balance
- Haskoin.Store: [bits] :: BinfoBlock -> !Word32
- Haskoin.Store: [blockHeight] :: BinfoTx -> !Maybe Word32
- Haskoin.Store: [blockIndex] :: BinfoTx -> !Maybe Word32
- Haskoin.Store: [blocks] :: HealthCheck -> !BlockHealth
- Haskoin.Store: [buy] :: BinfoTicker -> !Double
- Haskoin.Store: [cashAddr] :: BinfoMultiAddr -> !Bool
- Haskoin.Store: [change] :: XPubSummary -> !Word32
- Haskoin.Store: [code] :: BinfoSymbol -> !Text
- Haskoin.Store: [confirmations] :: BinfoUnspent -> !Int32
- Haskoin.Store: [confirmed] :: XPubSummary -> !Word64
- Haskoin.Store: [connected] :: BinfoInfo -> !Word32
- Haskoin.Store: [conversion] :: BinfoSymbol -> !Double
- Haskoin.Store: [count] :: MaxHealth -> !Int64
- Haskoin.Store: [crypto] :: BinfoInfo -> !BinfoSymbol
- Haskoin.Store: [date] :: BinfoHistory -> !Text
- Haskoin.Store: [deleted] :: TxData -> !Bool
- Haskoin.Store: [deriv] :: XPubSpec -> !DeriveType
- Haskoin.Store: [doubleSpend] :: BinfoTx -> !Bool
- Haskoin.Store: [external] :: XPubSummary -> !Word32
- Haskoin.Store: [fee] :: Transaction -> !Word64
- Haskoin.Store: [fiat] :: BinfoInfo -> !BinfoSymbol
- Haskoin.Store: [fifteen] :: BinfoTicker -> !Double
- Haskoin.Store: [filtered] :: BinfoWallet -> !Word64
- Haskoin.Store: [final] :: BinfoShortBal -> !Word64
- Haskoin.Store: [get] :: SerialList a -> [a]
- Haskoin.Store: [hash] :: BinfoHeader -> !BlockHash
- Haskoin.Store: [head] :: BinfoInfo -> !BinfoBlockInfo
- Haskoin.Store: [header] :: BlockData -> !BlockHeader
- Haskoin.Store: [headers] :: BlockHealth -> !BlockHeight
- Haskoin.Store: [height] :: BlockRef -> !BlockHeight
- Haskoin.Store: [index] :: Spender -> !Word32
- Haskoin.Store: [info] :: BinfoMultiAddr -> !BinfoInfo
- Haskoin.Store: [inputCount] :: BinfoTx -> !Word32
- Haskoin.Store: [input] :: BinfoSpender -> !Word32
- Haskoin.Store: [inputs] :: Transaction -> ![StoreInput]
- Haskoin.Store: [key] :: XPubSpec -> !XPubKey
- Haskoin.Store: [lastBlock] :: HealthCheck -> !TimeHealth
- Haskoin.Store: [lastTx] :: HealthCheck -> !TimeHealth
- Haskoin.Store: [last] :: BinfoTicker -> !Double
- Haskoin.Store: [local] :: BinfoSymbol -> !Bool
- Haskoin.Store: [locktime] :: Transaction -> !Word32
- Haskoin.Store: [main] :: BlockData -> !Bool
- Haskoin.Store: [maxKeys] :: CacheConfig -> !Integer
- Haskoin.Store: [max] :: TimeHealth -> !Int64
- Haskoin.Store: [merkle] :: BinfoBlock -> !Hash256
- Haskoin.Store: [metrics] :: CacheConfig -> !Maybe CacheMetrics
- Haskoin.Store: [min] :: CountHealth -> !Int64
- Haskoin.Store: [name] :: BinfoSymbol -> !Text
- Haskoin.Store: [network] :: HealthCheck -> !String
- Haskoin.Store: [next] :: BinfoBlock -> ![BlockHash]
- Haskoin.Store: [nonce] :: BinfoBlock -> !Word32
- Haskoin.Store: [ntx] :: BinfoShortBal -> !Word64
- Haskoin.Store: [outpoint] :: Unspent -> !OutPoint
- Haskoin.Store: [outputCount] :: BinfoTx -> !Word32
- Haskoin.Store: [output] :: BinfoTxInput -> !BinfoTxOutput
- Haskoin.Store: [outputs] :: Transaction -> ![StoreOutput]
- Haskoin.Store: [path] :: XPubUnspent -> ![KeyIndex]
- Haskoin.Store: [peers] :: HealthCheck -> !CountHealth
- Haskoin.Store: [pendingTxs] :: HealthCheck -> !MaxHealth
- Haskoin.Store: [pkscript] :: StoreInput -> !ByteString
- Haskoin.Store: [position] :: BlockRef -> !Word32
- Haskoin.Store: [prev] :: BinfoBlock -> !BlockHash
- Haskoin.Store: [prevs] :: TxData -> !IntMap Prev
- Haskoin.Store: [price] :: BinfoRate -> !Double
- Haskoin.Store: [rateThen] :: BinfoHistory -> !Double
- Haskoin.Store: [rbf] :: TxData -> !Bool
- Haskoin.Store: [received] :: XPubSummary -> !Word64
- Haskoin.Store: [recommendFee] :: BinfoMultiAddr -> !Bool
- Haskoin.Store: [redisMaxKeys] :: StoreConfig -> !Integer
- Haskoin.Store: [redisSyncInterval] :: StoreConfig -> !Int
- Haskoin.Store: [relay] :: PeerInfo -> !Bool
- Haskoin.Store: [relayed] :: BinfoTx -> !ByteString
- Haskoin.Store: [script] :: Unspent -> !ByteString
- Haskoin.Store: [sell] :: BinfoTicker -> !Double
- Haskoin.Store: [sent] :: BinfoWallet -> !Word64
- Haskoin.Store: [sequence] :: StoreInput -> !Word32
- Haskoin.Store: [services] :: PeerInfo -> !Word64
- Haskoin.Store: [size] :: Transaction -> !Word32
- Haskoin.Store: [spender] :: StoreOutput -> !Maybe Spender
- Haskoin.Store: [spenders] :: TxData -> !IntMap Spender
- Haskoin.Store: [spent] :: BinfoTxOutput -> !Bool
- Haskoin.Store: [subsidy] :: BlockData -> !Word64
- Haskoin.Store: [symbol] :: BinfoTicker -> !Text
- Haskoin.Store: [time] :: HealthCheck -> !Word64
- Haskoin.Store: [timestamp] :: TxData -> !Word64
- Haskoin.Store: [tx] :: TxData -> !Tx
- Haskoin.Store: [txid] :: TxRef -> !TxHash
- Haskoin.Store: [txids] :: BinfoHeader -> ![BinfoTxId]
- Haskoin.Store: [txidx] :: BinfoUnspent -> !BinfoTxId
- Haskoin.Store: [txs] :: BlockData -> ![TxHash]
- Haskoin.Store: [typ] :: BinfoTxOutput -> !Int
- Haskoin.Store: [unconfirmed] :: XPubSummary -> !Word64
- Haskoin.Store: [unspent] :: XPubUnspent -> !Unspent
- Haskoin.Store: [userAgent] :: PeerInfo -> !ByteString
- Haskoin.Store: [utxo] :: XPubSummary -> !Word64
- Haskoin.Store: [valueNow] :: BinfoHistory -> !Double
- Haskoin.Store: [valueThen] :: BinfoHistory -> !Double
- Haskoin.Store: [value] :: Unspent -> !Word64
- Haskoin.Store: [version] :: Transaction -> !Word32
- Haskoin.Store: [vol24] :: BinfoRate -> !Double
- Haskoin.Store: [wallet] :: BinfoMultiAddr -> !BinfoWallet
- Haskoin.Store: [weight] :: Transaction -> !Word32
- Haskoin.Store: [witness] :: StoreInput -> !WitnessStack
- Haskoin.Store: [work] :: BlockData -> !Integer
- Haskoin.Store: [xpub] :: BinfoUnspent -> !Maybe BinfoXPubPath
- Haskoin.Store: binfoHexValue :: Word64 -> Text
- Haskoin.Store: class Healthy a
- Haskoin.Store: class Monad m => StoreReadBase (m :: Type -> Type)
- Haskoin.Store: class StoreReadBase m => StoreReadExtra (m :: Type -> Type)
- Haskoin.Store: confirmed :: BlockRef -> Bool
- Haskoin.Store: connectRedis :: MonadIO m => String -> m Connection
- Haskoin.Store: data Balance
- Haskoin.Store: data BinfoAddr
- Haskoin.Store: data BinfoBalance
- Haskoin.Store: data BinfoBlock
- Haskoin.Store: data BinfoBlockId
- Haskoin.Store: data BinfoBlockInfo
- Haskoin.Store: data BinfoFilter
- Haskoin.Store: data BinfoHeader
- Haskoin.Store: data BinfoHistory
- Haskoin.Store: data BinfoInfo
- Haskoin.Store: data BinfoMultiAddr
- Haskoin.Store: data BinfoRate
- Haskoin.Store: data BinfoRawAddr
- Haskoin.Store: data BinfoShortBal
- Haskoin.Store: data BinfoSpender
- Haskoin.Store: data BinfoSymbol
- Haskoin.Store: data BinfoTicker
- Haskoin.Store: data BinfoTx
- Haskoin.Store: data BinfoTxId
- Haskoin.Store: data BinfoTxInput
- Haskoin.Store: data BinfoTxOutput
- Haskoin.Store: data BinfoUnspent
- Haskoin.Store: data BinfoWallet
- Haskoin.Store: data BinfoXPubPath
- Haskoin.Store: data BlockData
- Haskoin.Store: data BlockHealth
- Haskoin.Store: data BlockRef
- Haskoin.Store: data CacheError
- Haskoin.Store: data CountHealth
- Haskoin.Store: data DeriveType
- Haskoin.Store: data Event
- Haskoin.Store: data Except
- Haskoin.Store: data HealthCheck
- Haskoin.Store: data MaxHealth
- Haskoin.Store: data PeerInfo
- Haskoin.Store: data Prev
- Haskoin.Store: data Spender
- Haskoin.Store: data StoreInput
- Haskoin.Store: data StoreOutput
- Haskoin.Store: data TimeHealth
- Haskoin.Store: data Transaction
- Haskoin.Store: data TxData
- Haskoin.Store: data TxRef
- Haskoin.Store: data Unspent
- Haskoin.Store: data XPubBal
- Haskoin.Store: data XPubSpec
- Haskoin.Store: data XPubSummary
- Haskoin.Store: data XPubUnspent
- Haskoin.Store: deriveTypeToText :: DeriveType -> Text
- Haskoin.Store: encodeBinfoTxId :: Bool -> TxHash -> BinfoTxId
- Haskoin.Store: fromTransaction :: Transaction -> TxData
- Haskoin.Store: getAddressTxs :: StoreReadExtra m => Address -> Limits -> m [TxRef]
- Haskoin.Store: getAddressUnspents :: StoreReadExtra m => Address -> Limits -> m [Unspent]
- Haskoin.Store: getAddressesTxs :: StoreReadExtra m => [Address] -> Limits -> m [TxRef]
- Haskoin.Store: getAddressesUnspents :: StoreReadExtra m => [Address] -> Limits -> m [Unspent]
- Haskoin.Store: getBalance :: StoreReadBase m => Address -> m (Maybe Balance)
- Haskoin.Store: getBalances :: StoreReadExtra m => [Address] -> m [Balance]
- Haskoin.Store: getBestBlock :: StoreReadBase m => m (Maybe BlockHash)
- Haskoin.Store: getBlock :: StoreReadBase m => BlockHash -> m (Maybe BlockData)
- Haskoin.Store: getBlocksAtHeight :: StoreReadBase m => BlockHeight -> m [BlockHash]
- Haskoin.Store: getCtx :: StoreReadBase m => m Ctx
- Haskoin.Store: getInitialGap :: StoreReadExtra m => m Word32
- Haskoin.Store: getManyTxData :: StoreReadBase m => [TxHash] -> m [Maybe TxData]
- Haskoin.Store: getMaxGap :: StoreReadExtra m => m Word32
- Haskoin.Store: getMempool :: StoreReadBase m => m [(UnixTime, TxHash)]
- Haskoin.Store: getMempoolPage :: StoreReadBase m => Limits -> m [(UnixTime, TxHash)]
- Haskoin.Store: getNetwork :: StoreReadBase m => m Network
- Haskoin.Store: getNumTxData :: StoreReadExtra m => Word64 -> m [TxData]
- Haskoin.Store: getSpender :: StoreReadBase m => OutPoint -> m (Maybe Spender)
- Haskoin.Store: getTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)
- Haskoin.Store: getUnixTime :: MonadGet m => m Word64
- Haskoin.Store: getUnspent :: StoreReadBase m => OutPoint -> m (Maybe Unspent)
- Haskoin.Store: isCoinbase :: StoreInput -> Bool
- Haskoin.Store: isCoinbaseTx :: Tx -> Bool
- Haskoin.Store: isInCache :: forall (m :: Type -> Type). MonadIO m => XPubSpec -> CacheT m Bool
- Haskoin.Store: isOK :: Healthy a => a -> Bool
- Haskoin.Store: newtype BinfoBlockInfos
- Haskoin.Store: newtype BinfoDate
- Haskoin.Store: newtype BinfoMempool
- Haskoin.Store: newtype BinfoUnspents
- Haskoin.Store: newtype GenericResult a
- Haskoin.Store: newtype RawResult a
- Haskoin.Store: newtype RawResultList a
- Haskoin.Store: newtype SerialList a
- Haskoin.Store: newtype TxId
- Haskoin.Store: nullBalance :: Balance -> Bool
- Haskoin.Store: parseBinfoAddr :: Network -> Ctx -> Text -> Maybe [BinfoAddr]
- Haskoin.Store: putUnixTime :: MonadPut m => Word64 -> m ()
- Haskoin.Store: relevantTxs :: HashSet Address -> Bool -> Transaction -> HashSet TxHash
- Haskoin.Store: textToDeriveType :: Text -> Maybe DeriveType
- Haskoin.Store: toBinfoAddrs :: HashMap Address Balance -> HashMap XPubSpec [XPubBal] -> HashMap XPubSpec Word64 -> [BinfoBalance]
- Haskoin.Store: toBinfoBlock :: BlockData -> [BinfoTx] -> [BlockHash] -> BinfoBlock
- Haskoin.Store: toBinfoBlockInfo :: BlockData -> BinfoBlockInfo
- Haskoin.Store: toBinfoHistory :: Int64 -> Word64 -> Double -> Double -> Word64 -> TxHash -> BinfoHistory
- Haskoin.Store: toBinfoTx :: Bool -> HashMap Address (Maybe BinfoXPubPath) -> Bool -> Int64 -> Transaction -> BinfoTx
- Haskoin.Store: toBinfoTxSimple :: Bool -> Transaction -> BinfoTx
- Haskoin.Store: toTransaction :: Network -> Ctx -> TxData -> Transaction
- Haskoin.Store: transactionData :: Transaction -> Tx
- Haskoin.Store: txDataFee :: TxData -> Word64
- Haskoin.Store: type BlockPos = Word32
- Haskoin.Store: type CacheT = ReaderT Maybe CacheConfig
- Haskoin.Store: type UnixTime = Word64
- Haskoin.Store: withCache :: Maybe CacheConfig -> CacheT m a -> m a
- Haskoin.Store: xPubBals :: StoreReadExtra m => XPubSpec -> m [XPubBal]
- Haskoin.Store: xPubTxCount :: StoreReadExtra m => XPubSpec -> [XPubBal] -> m Word32
- Haskoin.Store: xPubTxs :: StoreReadExtra m => XPubSpec -> [XPubBal] -> Limits -> m [TxRef]
- Haskoin.Store: xPubUnspents :: StoreReadExtra m => XPubSpec -> [XPubBal] -> Limits -> m [XPubUnspent]
- Haskoin.Store: zeroBalance :: Address -> Balance
- Haskoin.Store.BlockStore: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.BlockStore.BlockT m)
- Haskoin.Store.BlockStore: instance GHC.Classes.Eq Haskoin.Store.BlockStore.PendingTx
- Haskoin.Store.BlockStore: instance GHC.Classes.Ord Haskoin.Store.BlockStore.PendingTx
- Haskoin.Store.BlockStore: instance GHC.Internal.Control.Monad.IO.Class.MonadIO m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.BlockStore.BlockT m)
- Haskoin.Store.BlockStore: instance GHC.Internal.Show.Show Haskoin.Store.BlockStore.PendingTx
- Haskoin.Store.Cache: LogicError :: !String -> CacheError
- Haskoin.Store.Cache: RedisError :: Reply -> CacheError
- Haskoin.Store.Cache: RedisTxError :: !String -> CacheError
- Haskoin.Store.Cache: [chain] :: CacheConfig -> !Chain
- Haskoin.Store.Cache: [logger] :: CacheConfig -> !Logger
- Haskoin.Store.Cache: [maxKeys] :: CacheConfig -> !Integer
- Haskoin.Store.Cache: [metrics] :: CacheConfig -> !Maybe CacheMetrics
- Haskoin.Store.Cache: [redis] :: CacheConfig -> !Connection
- Haskoin.Store.Cache: cacheDelXPubs :: forall (m :: Type -> Type). (MonadIO m, StoreReadBase m) => [XPubSpec] -> CacheT m Integer
- Haskoin.Store.Cache: cacheNewBlock :: MonadIO m => CacheWriter -> m ()
- Haskoin.Store.Cache: cacheNewTx :: MonadIO m => TxHash -> CacheWriter -> m ()
- Haskoin.Store.Cache: cacheSyncMempool :: MonadIO m => CacheWriter -> m ()
- Haskoin.Store.Cache: cacheWriter :: (MonadUnliftIO m, StoreReadExtra m) => CacheConfig -> CacheWriterInbox -> m ()
- Haskoin.Store.Cache: connectRedis :: MonadIO m => String -> m Connection
- Haskoin.Store.Cache: data CacheError
- Haskoin.Store.Cache: data CacheMetrics
- Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Haskoin.Store.Common.StoreReadExtra m) => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.Cache.CacheT m)
- Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Haskoin.Store.Common.StoreReadExtra m) => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.Cache.CacheX m)
- Haskoin.Store.Cache: instance Control.DeepSeq.NFData Haskoin.Store.Cache.AddressXPub
- Haskoin.Store.Cache: instance Control.DeepSeq.NFData Haskoin.Store.Cache.CacheError
- Haskoin.Store.Cache: instance Data.Serialize.Serialize Haskoin.Store.Cache.AddressXPub
- Haskoin.Store.Cache: instance GHC.Classes.Eq Haskoin.Store.Cache.AddressXPub
- Haskoin.Store.Cache: instance GHC.Classes.Eq Haskoin.Store.Cache.CacheError
- Haskoin.Store.Cache: instance GHC.Internal.Exception.Type.Exception Haskoin.Store.Cache.CacheError
- Haskoin.Store.Cache: instance GHC.Internal.Generics.Generic Haskoin.Store.Cache.AddressXPub
- Haskoin.Store.Cache: instance GHC.Internal.Generics.Generic Haskoin.Store.Cache.CacheError
- Haskoin.Store.Cache: instance GHC.Internal.Show.Show Haskoin.Store.Cache.AddressXPub
- Haskoin.Store.Cache: instance GHC.Internal.Show.Show Haskoin.Store.Cache.CacheError
- Haskoin.Store.Cache: instance Haskoin.Store.Common.StoreReadBase m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Cache.CacheT m)
- Haskoin.Store.Cache: instance Haskoin.Store.Common.StoreReadBase m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Cache.CacheX m)
- Haskoin.Store.Cache: isInCache :: forall (m :: Type -> Type). MonadIO m => XPubSpec -> CacheT m Bool
- Haskoin.Store.Cache: newCacheMetrics :: MonadIO m => Logger -> Stats -> Connection -> DatabaseReader -> m CacheMetrics
- Haskoin.Store.Cache: type CacheT = ReaderT Maybe CacheConfig
- Haskoin.Store.Cache: type CacheWriter = Mailbox CacheWriterMessage
- Haskoin.Store.Cache: type CacheWriterInbox = Inbox CacheWriterMessage
- Haskoin.Store.Common: addToMempool :: StoreWrite m => TxHash -> UnixTime -> m ()
- Haskoin.Store.Common: class Monad m => StoreReadBase (m :: Type -> Type)
- Haskoin.Store.Common: class StoreReadBase m => StoreReadExtra (m :: Type -> Type)
- Haskoin.Store.Common: class StoreWrite (m :: Type -> Type)
- Haskoin.Store.Common: deleteAddrTx :: StoreWrite m => Address -> TxRef -> m ()
- Haskoin.Store.Common: deleteAddrUnspent :: StoreWrite m => Address -> Unspent -> m ()
- Haskoin.Store.Common: deleteFromMempool :: StoreWrite m => TxHash -> m ()
- Haskoin.Store.Common: deleteUnspent :: StoreWrite m => OutPoint -> m ()
- Haskoin.Store.Common: getAddressTxs :: StoreReadExtra m => Address -> Limits -> m [TxRef]
- Haskoin.Store.Common: getAddressUnspents :: StoreReadExtra m => Address -> Limits -> m [Unspent]
- Haskoin.Store.Common: getAddressesTxs :: StoreReadExtra m => [Address] -> Limits -> m [TxRef]
- Haskoin.Store.Common: getAddressesUnspents :: StoreReadExtra m => [Address] -> Limits -> m [Unspent]
- Haskoin.Store.Common: getBalance :: StoreReadBase m => Address -> m (Maybe Balance)
- Haskoin.Store.Common: getBalances :: StoreReadExtra m => [Address] -> m [Balance]
- Haskoin.Store.Common: getBestBlock :: StoreReadBase m => m (Maybe BlockHash)
- Haskoin.Store.Common: getBlock :: StoreReadBase m => BlockHash -> m (Maybe BlockData)
- Haskoin.Store.Common: getBlocksAtHeight :: StoreReadBase m => BlockHeight -> m [BlockHash]
- Haskoin.Store.Common: getCtx :: StoreReadBase m => m Ctx
- Haskoin.Store.Common: getInitialGap :: StoreReadExtra m => m Word32
- Haskoin.Store.Common: getManyTxData :: StoreReadBase m => [TxHash] -> m [Maybe TxData]
- Haskoin.Store.Common: getMaxGap :: StoreReadExtra m => m Word32
- Haskoin.Store.Common: getMempool :: StoreReadBase m => m [(UnixTime, TxHash)]
- Haskoin.Store.Common: getMempoolPage :: StoreReadBase m => Limits -> m [(UnixTime, TxHash)]
- Haskoin.Store.Common: getNetwork :: StoreReadBase m => m Network
- Haskoin.Store.Common: getNumTxData :: StoreReadExtra m => Word64 -> m [TxData]
- Haskoin.Store.Common: getSpender :: StoreReadBase m => OutPoint -> m (Maybe Spender)
- Haskoin.Store.Common: getTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)
- Haskoin.Store.Common: getUnspent :: StoreReadBase m => OutPoint -> m (Maybe Unspent)
- Haskoin.Store.Common: insertAddrTx :: StoreWrite m => Address -> TxRef -> m ()
- Haskoin.Store.Common: insertAddrUnspent :: StoreWrite m => Address -> Unspent -> m ()
- Haskoin.Store.Common: insertBlock :: StoreWrite m => BlockData -> m ()
- Haskoin.Store.Common: insertTx :: StoreWrite m => TxData -> m ()
- Haskoin.Store.Common: insertUnspent :: StoreWrite m => Unspent -> m ()
- Haskoin.Store.Common: setBalance :: StoreWrite m => Balance -> m ()
- Haskoin.Store.Common: setBest :: StoreWrite m => BlockHash -> m ()
- Haskoin.Store.Common: setBlocksAtHeight :: StoreWrite m => [BlockHash] -> BlockHeight -> m ()
- Haskoin.Store.Common: xPubBals :: StoreReadExtra m => XPubSpec -> m [XPubBal]
- Haskoin.Store.Common: xPubTxCount :: StoreReadExtra m => XPubSpec -> [XPubBal] -> m Word32
- Haskoin.Store.Common: xPubTxs :: StoreReadExtra m => XPubSpec -> [XPubBal] -> Limits -> m [TxRef]
- Haskoin.Store.Common: xPubUnspents :: StoreReadExtra m => XPubSpec -> [XPubBal] -> Limits -> m [XPubUnspent]
- Haskoin.Store.Database.Reader: getMempoolBefore :: forall (m :: Type -> Type). MonadIO m => UnixTime -> Int -> DatabaseReaderT m [(UnixTime, TxHash)]
- Haskoin.Store.Database.Reader: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.Database.Reader.DatabaseReaderT m)
- Haskoin.Store.Database.Reader: instance GHC.Internal.Control.Monad.IO.Class.MonadIO m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Database.Reader.DatabaseReaderT m)
- Haskoin.Store.Database.Reader: type DatabaseReaderT = ReaderT DatabaseReader
- Haskoin.Store.Database.Reader: withDB :: DatabaseReader -> DatabaseReaderT m a -> m a
- Haskoin.Store.Database.Writer: instance GHC.Internal.Control.Monad.IO.Class.MonadIO m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Database.Writer.WriterT m)
- Haskoin.Store.Database.Writer: instance GHC.Internal.Control.Monad.IO.Class.MonadIO m => Haskoin.Store.Common.StoreWrite (Haskoin.Store.Database.Writer.WriterT m)
- Haskoin.Store.Database.Writer: type WriterT = ReaderT Writer
- Haskoin.Store.Logic: type ImportM = WriterT IO
- Haskoin.Store.Manager: [redisMaxKeys] :: StoreConfig -> !Integer
- Haskoin.Store.Manager: [redisSyncInterval] :: StoreConfig -> !Int
- Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadBase (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
- Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Web.ActionT m)
- Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadExtra (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
- Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.Web.ActionT m)
+ Haskoin.Store: StoreRead :: !Network -> !Ctx -> IO (Maybe BlockHash) -> (BlockHeight -> IO [BlockHash]) -> (BlockHash -> IO (Maybe BlockData)) -> (TxHash -> IO (Maybe TxData)) -> ([TxHash] -> IO [Maybe TxData]) -> (OutPoint -> IO (Maybe Spender)) -> (Address -> IO (Maybe Balance)) -> (OutPoint -> IO (Maybe Unspent)) -> IO [(UnixTime, TxHash)] -> (Limits -> IO [(UnixTime, TxHash)]) -> ([Address] -> Limits -> IO [TxRef]) -> ([Address] -> Limits -> IO [Unspent]) -> !Word32 -> !Word32 -> (Word64 -> IO [TxData]) -> ([Address] -> IO [Balance]) -> (Address -> Limits -> IO [TxRef]) -> (Address -> Limits -> IO [Unspent]) -> (XPubSpec -> IO [XPubBal]) -> (XPubSpec -> [XPubBal] -> Limits -> IO [XPubUnspent]) -> (XPubSpec -> [XPubBal] -> Limits -> IO [TxRef]) -> (XPubSpec -> [XPubBal] -> IO Word32) -> ([Address] -> IO Word32) -> StoreRead
+ Haskoin.Store: [events] :: Store -> !Publisher StoreEvent
+ Haskoin.Store: [fleet] :: StoreConfig -> !Bool
+ Haskoin.Store: [getAddressTxs] :: StoreRead -> Address -> Limits -> IO [TxRef]
+ Haskoin.Store: [getAddressUnspents] :: StoreRead -> Address -> Limits -> IO [Unspent]
+ Haskoin.Store: [getAddressesTxCount] :: StoreRead -> [Address] -> IO Word32
+ Haskoin.Store: [getAddressesTxs] :: StoreRead -> [Address] -> Limits -> IO [TxRef]
+ Haskoin.Store: [getAddressesUnspents] :: StoreRead -> [Address] -> Limits -> IO [Unspent]
+ Haskoin.Store: [getBalance] :: StoreRead -> Address -> IO (Maybe Balance)
+ Haskoin.Store: [getBalances] :: StoreRead -> [Address] -> IO [Balance]
+ Haskoin.Store: [getBestBlock] :: StoreRead -> IO (Maybe BlockHash)
+ Haskoin.Store: [getBlock] :: StoreRead -> BlockHash -> IO (Maybe BlockData)
+ Haskoin.Store: [getBlocksAtHeight] :: StoreRead -> BlockHeight -> IO [BlockHash]
+ Haskoin.Store: [getManyTxData] :: StoreRead -> [TxHash] -> IO [Maybe TxData]
+ Haskoin.Store: [getMempoolPage] :: StoreRead -> Limits -> IO [(UnixTime, TxHash)]
+ Haskoin.Store: [getMempool] :: StoreRead -> IO [(UnixTime, TxHash)]
+ Haskoin.Store: [getNumTxData] :: StoreRead -> Word64 -> IO [TxData]
+ Haskoin.Store: [getSpender] :: StoreRead -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store: [getTxData] :: StoreRead -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store: [getUnspent] :: StoreRead -> OutPoint -> IO (Maybe Unspent)
+ Haskoin.Store: [initialGap] :: StoreRead -> !Word32
+ Haskoin.Store: [maxGap] :: StoreRead -> !Word32
+ Haskoin.Store: [maxXPubs] :: CacheConfig -> !Int
+ Haskoin.Store: [minTxs] :: CacheConfig -> !Int
+ Haskoin.Store: [redisMaxXPubs] :: StoreConfig -> !Int
+ Haskoin.Store: [redisMinTxs] :: StoreConfig -> !Int
+ Haskoin.Store: [url] :: CacheConfig -> !String
+ Haskoin.Store: [xPubBals] :: StoreRead -> XPubSpec -> IO [XPubBal]
+ Haskoin.Store: [xPubTxCount] :: StoreRead -> XPubSpec -> [XPubBal] -> IO Word32
+ Haskoin.Store: [xPubTxs] :: StoreRead -> XPubSpec -> [XPubBal] -> Limits -> IO [TxRef]
+ Haskoin.Store: [xPubUnspents] :: StoreRead -> XPubSpec -> [XPubBal] -> Limits -> IO [XPubUnspent]
+ Haskoin.Store: cacheDelete :: Cache -> XPubSpec -> IO Bool
+ Haskoin.Store: cacheEvict :: Cache -> Int -> IO Int
+ Haskoin.Store: cacheIsCached :: Cache -> XPubSpec -> IO Bool
+ Haskoin.Store: data Cache
+ Haskoin.Store: data StoreRead
+ Haskoin.Store: databaseRead :: DatabaseReader -> StoreRead
+ Haskoin.Store.BlockStore: [onBest] :: BlockStoreConfig -> !BlockHash -> IO ()
+ Haskoin.Store.BlockStore.Pending: PendingTx :: !UTCTime -> !Tx -> !HashSet TxHash -> PendingTx
+ Haskoin.Store.BlockStore.Pending: [deps] :: PendingTx -> !HashSet TxHash
+ Haskoin.Store.BlockStore.Pending: [time] :: PendingTx -> !UTCTime
+ Haskoin.Store.BlockStore.Pending: [tx] :: PendingTx -> !Tx
+ Haskoin.Store.BlockStore.Pending: data Pending
+ Haskoin.Store.BlockStore.Pending: data PendingTx
+ Haskoin.Store.BlockStore.Pending: empty :: Pending
+ Haskoin.Store.BlockStore.Pending: imported :: TxHash -> Pending -> Pending
+ Haskoin.Store.BlockStore.Pending: instance GHC.Classes.Eq Haskoin.Store.BlockStore.Pending.Pending
+ Haskoin.Store.BlockStore.Pending: instance GHC.Classes.Eq Haskoin.Store.BlockStore.Pending.PendingTx
+ Haskoin.Store.BlockStore.Pending: instance GHC.Classes.Ord Haskoin.Store.BlockStore.Pending.PendingTx
+ Haskoin.Store.BlockStore.Pending: instance GHC.Internal.Show.Show Haskoin.Store.BlockStore.Pending.Pending
+ Haskoin.Store.BlockStore.Pending: instance GHC.Internal.Show.Show Haskoin.Store.BlockStore.Pending.PendingTx
+ Haskoin.Store.BlockStore.Pending: isPending :: TxHash -> Pending -> Bool
+ Haskoin.Store.BlockStore.Pending: orphanTimeout :: NominalDiffTime
+ Haskoin.Store.BlockStore.Pending: prune :: UTCTime -> Pending -> Pending
+ Haskoin.Store.BlockStore.Pending: queued :: Pending -> Int
+ Haskoin.Store.BlockStore.Pending: received :: UTCTime -> Tx -> HashSet TxHash -> Pending -> Pending
+ Haskoin.Store.BlockStore.Pending: refresh :: HashMap TxHash (Maybe (HashSet TxHash)) -> Pending -> Pending
+ Haskoin.Store.BlockStore.Pending: requestTimeout :: NominalDiffTime
+ Haskoin.Store.BlockStore.Pending: requested :: UTCTime -> [TxHash] -> Pending -> Pending
+ Haskoin.Store.BlockStore.Pending: size :: Pending -> Int
+ Haskoin.Store.BlockStore.Pending: takeReady :: Int -> Pending -> ([PendingTx], Pending)
+ Haskoin.Store.BlockStore.Pending: waiting :: Pending -> [PendingTx]
+ Haskoin.Store.BlockStore.Sync: Download :: !p -> !UTCTime -> ![BlockHash] -> Download p
+ Haskoin.Store.BlockStore.Sync: Idle :: Sync p
+ Haskoin.Store.BlockStore.Sync: NotRequested :: Accept
+ Haskoin.Store.BlockStore.Sync: NotSyncingPeer :: Accept
+ Haskoin.Store.BlockStore.Sync: Requested :: Accept
+ Haskoin.Store.BlockStore.Sync: Syncing :: !Download p -> Sync p
+ Haskoin.Store.BlockStore.Sync: [blocks] :: Download p -> ![BlockHash]
+ Haskoin.Store.BlockStore.Sync: [peer] :: Download p -> !p
+ Haskoin.Store.BlockStore.Sync: [time] :: Download p -> !UTCTime
+ Haskoin.Store.BlockStore.Sync: accept :: Eq p => p -> BlockHash -> Sync p -> Accept
+ Haskoin.Store.BlockStore.Sync: data Accept
+ Haskoin.Store.BlockStore.Sync: data Download p
+ Haskoin.Store.BlockStore.Sync: data Sync p
+ Haskoin.Store.BlockStore.Sync: forgetRequests :: Sync p -> Sync p
+ Haskoin.Store.BlockStore.Sync: imported :: UTCTime -> BlockHash -> Sync p -> Sync p
+ Haskoin.Store.BlockStore.Sync: instance GHC.Classes.Eq Haskoin.Store.BlockStore.Sync.Accept
+ Haskoin.Store.BlockStore.Sync: instance GHC.Classes.Eq p => GHC.Classes.Eq (Haskoin.Store.BlockStore.Sync.Download p)
+ Haskoin.Store.BlockStore.Sync: instance GHC.Classes.Eq p => GHC.Classes.Eq (Haskoin.Store.BlockStore.Sync.Sync p)
+ Haskoin.Store.BlockStore.Sync: instance GHC.Internal.Show.Show Haskoin.Store.BlockStore.Sync.Accept
+ Haskoin.Store.BlockStore.Sync: instance GHC.Internal.Show.Show p => GHC.Internal.Show.Show (Haskoin.Store.BlockStore.Sync.Download p)
+ Haskoin.Store.BlockStore.Sync: instance GHC.Internal.Show.Show p => GHC.Internal.Show.Show (Haskoin.Store.BlockStore.Sync.Sync p)
+ Haskoin.Store.BlockStore.Sync: lastRequested :: Sync p -> Maybe BlockHash
+ Haskoin.Store.BlockStore.Sync: needsMoreBlocks :: Int -> Int -> Bool
+ Haskoin.Store.BlockStore.Sync: outstanding :: Sync p -> [BlockHash]
+ Haskoin.Store.BlockStore.Sync: release :: Eq p => p -> Sync p -> (Bool, Sync p)
+ Haskoin.Store.BlockStore.Sync: request :: [BlockHash] -> Sync p -> Sync p
+ Haskoin.Store.BlockStore.Sync: start :: UTCTime -> p -> Sync p -> Maybe (Sync p)
+ Haskoin.Store.BlockStore.Sync: syncingPeer :: Sync p -> Maybe p
+ Haskoin.Store.BlockStore.Sync: timedOut :: NominalDiffTime -> UTCTime -> Sync p -> Maybe p
+ Haskoin.Store.BlockStore.Sync: wantsMore :: Int -> Sync p -> Maybe p
+ Haskoin.Store.Cache: Behind :: Reload
+ Haskoin.Store.Cache: CacheCounters :: !Int -> !Int -> !Int -> CacheCounters
+ Haskoin.Store.Cache: Locked :: Reload
+ Haskoin.Store.Cache: Reloaded :: Reload
+ Haskoin.Store.Cache: [behind] :: CacheCounters -> !Int
+ Haskoin.Store.Cache: [fullReloads] :: CacheCounters -> !Int
+ Haskoin.Store.Cache: [maxXPubs] :: CacheConfig -> !Int
+ Haskoin.Store.Cache: [minTxs] :: CacheConfig -> !Int
+ Haskoin.Store.Cache: [reloads] :: CacheCounters -> !Int
+ Haskoin.Store.Cache: [stats] :: CacheConfig -> !Maybe Stats
+ Haskoin.Store.Cache: [url] :: CacheConfig -> !String
+ Haskoin.Store.Cache: cacheAlive :: Cache -> IO Bool
+ Haskoin.Store.Cache: cacheCounters :: Cache -> IO CacheCounters
+ Haskoin.Store.Cache: cacheDelete :: Cache -> XPubSpec -> IO Bool
+ Haskoin.Store.Cache: cacheEvict :: Cache -> Int -> IO Int
+ Haskoin.Store.Cache: cacheIsCached :: Cache -> XPubSpec -> IO Bool
+ Haskoin.Store.Cache: cachePrefix :: Network -> ByteString
+ Haskoin.Store.Cache: cacheRefresh :: Cache -> XPubSpec -> HashSet TxHash -> IO Reload
+ Haskoin.Store.Cache: cacheRequest :: Cache -> XPubSpec -> [XPubBal] -> IO ()
+ Haskoin.Store.Cache: cacheTxData :: Cache -> [TxHash] -> IO [Maybe TxData]
+ Haskoin.Store.Cache: cacheXPubBals :: Cache -> XPubSpec -> IO (Maybe [XPubBal])
+ Haskoin.Store.Cache: cacheXPubTxCount :: Cache -> XPubSpec -> IO (Maybe Word32)
+ Haskoin.Store.Cache: cacheXPubTxs :: Cache -> XPubSpec -> Limits -> IO (Maybe [TxRef])
+ Haskoin.Store.Cache: cacheXPubUnspents :: Cache -> XPubSpec -> [XPubBal] -> Limits -> IO (Maybe [XPubUnspent])
+ Haskoin.Store.Cache: data Cache
+ Haskoin.Store.Cache: data CacheCounters
+ Haskoin.Store.Cache: data Reload
+ Haskoin.Store.Cache: fullReloadAfter :: XPubSpec -> Integer
+ Haskoin.Store.Cache: fullReloadEvery :: Integer
+ Haskoin.Store.Cache: instance GHC.Classes.Eq Haskoin.Store.Cache.CacheCounters
+ Haskoin.Store.Cache: instance GHC.Internal.Show.Show Haskoin.Store.Cache.CacheCounters
+ Haskoin.Store.Cache: xpubId :: XPubSpec -> ByteString
+ Haskoin.Store.Cache.Protocol: Behind :: Reload
+ Haskoin.Store.Cache.Protocol: Busy :: Claim
+ Haskoin.Store.Cache.Protocol: ClaimLater :: OnClaim
+ Haskoin.Store.Cache.Protocol: Claimed :: Claim
+ Haskoin.Store.Cache.Protocol: Done :: Claim
+ Haskoin.Store.Cache.Protocol: Finish :: OnReload
+ Haskoin.Store.Cache.Protocol: Hold :: OnReload
+ Haskoin.Store.Cache.Protocol: Locked :: Reload
+ Haskoin.Store.Cache.Protocol: Release :: OnReload
+ Haskoin.Store.Cache.Protocol: ReloadNow :: OnClaim
+ Haskoin.Store.Cache.Protocol: Reloaded :: Reload
+ Haskoin.Store.Cache.Protocol: Skip :: OnClaim
+ Haskoin.Store.Cache.Protocol: afterClaim :: Claim -> OnClaim
+ Haskoin.Store.Cache.Protocol: afterReload :: Reload -> OnReload
+ Haskoin.Store.Cache.Protocol: data Claim
+ Haskoin.Store.Cache.Protocol: data OnClaim
+ Haskoin.Store.Cache.Protocol: data OnReload
+ Haskoin.Store.Cache.Protocol: data Reload
+ Haskoin.Store.Cache.Protocol: instance GHC.Classes.Eq Haskoin.Store.Cache.Protocol.Claim
+ Haskoin.Store.Cache.Protocol: instance GHC.Classes.Eq Haskoin.Store.Cache.Protocol.OnClaim
+ Haskoin.Store.Cache.Protocol: instance GHC.Classes.Eq Haskoin.Store.Cache.Protocol.OnReload
+ Haskoin.Store.Cache.Protocol: instance GHC.Classes.Eq Haskoin.Store.Cache.Protocol.Reload
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Enum.Bounded Haskoin.Store.Cache.Protocol.Claim
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Enum.Bounded Haskoin.Store.Cache.Protocol.Reload
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Enum.Enum Haskoin.Store.Cache.Protocol.Claim
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Enum.Enum Haskoin.Store.Cache.Protocol.Reload
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Show.Show Haskoin.Store.Cache.Protocol.Claim
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Show.Show Haskoin.Store.Cache.Protocol.OnClaim
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Show.Show Haskoin.Store.Cache.Protocol.OnReload
+ Haskoin.Store.Cache.Protocol: instance GHC.Internal.Show.Show Haskoin.Store.Cache.Protocol.Reload
+ Haskoin.Store.Cache.Protocol: mayReplace :: Maybe Integer -> BlockHeight -> Bool -> Bool
+ Haskoin.Store.Common: StoreRead :: !Network -> !Ctx -> IO (Maybe BlockHash) -> (BlockHeight -> IO [BlockHash]) -> (BlockHash -> IO (Maybe BlockData)) -> (TxHash -> IO (Maybe TxData)) -> ([TxHash] -> IO [Maybe TxData]) -> (OutPoint -> IO (Maybe Spender)) -> (Address -> IO (Maybe Balance)) -> (OutPoint -> IO (Maybe Unspent)) -> IO [(UnixTime, TxHash)] -> (Limits -> IO [(UnixTime, TxHash)]) -> ([Address] -> Limits -> IO [TxRef]) -> ([Address] -> Limits -> IO [Unspent]) -> !Word32 -> !Word32 -> (Word64 -> IO [TxData]) -> ([Address] -> IO [Balance]) -> (Address -> Limits -> IO [TxRef]) -> (Address -> Limits -> IO [Unspent]) -> (XPubSpec -> IO [XPubBal]) -> (XPubSpec -> [XPubBal] -> Limits -> IO [XPubUnspent]) -> (XPubSpec -> [XPubBal] -> Limits -> IO [TxRef]) -> (XPubSpec -> [XPubBal] -> IO Word32) -> ([Address] -> IO Word32) -> StoreRead
+ Haskoin.Store.Common: [ctx] :: StoreRead -> !Ctx
+ Haskoin.Store.Common: [getAddressTxs] :: StoreRead -> Address -> Limits -> IO [TxRef]
+ Haskoin.Store.Common: [getAddressUnspents] :: StoreRead -> Address -> Limits -> IO [Unspent]
+ Haskoin.Store.Common: [getAddressesTxCount] :: StoreRead -> [Address] -> IO Word32
+ Haskoin.Store.Common: [getAddressesTxs] :: StoreRead -> [Address] -> Limits -> IO [TxRef]
+ Haskoin.Store.Common: [getAddressesUnspents] :: StoreRead -> [Address] -> Limits -> IO [Unspent]
+ Haskoin.Store.Common: [getBalance] :: StoreRead -> Address -> IO (Maybe Balance)
+ Haskoin.Store.Common: [getBalances] :: StoreRead -> [Address] -> IO [Balance]
+ Haskoin.Store.Common: [getBestBlock] :: StoreRead -> IO (Maybe BlockHash)
+ Haskoin.Store.Common: [getBlock] :: StoreRead -> BlockHash -> IO (Maybe BlockData)
+ Haskoin.Store.Common: [getBlocksAtHeight] :: StoreRead -> BlockHeight -> IO [BlockHash]
+ Haskoin.Store.Common: [getManyTxData] :: StoreRead -> [TxHash] -> IO [Maybe TxData]
+ Haskoin.Store.Common: [getMempoolPage] :: StoreRead -> Limits -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Common: [getMempool] :: StoreRead -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Common: [getNumTxData] :: StoreRead -> Word64 -> IO [TxData]
+ Haskoin.Store.Common: [getSpender] :: StoreRead -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Common: [getTxData] :: StoreRead -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Common: [getUnspent] :: StoreRead -> OutPoint -> IO (Maybe Unspent)
+ Haskoin.Store.Common: [initialGap] :: StoreRead -> !Word32
+ Haskoin.Store.Common: [maxGap] :: StoreRead -> !Word32
+ Haskoin.Store.Common: [net] :: StoreRead -> !Network
+ Haskoin.Store.Common: [xPubBals] :: StoreRead -> XPubSpec -> IO [XPubBal]
+ Haskoin.Store.Common: [xPubTxCount] :: StoreRead -> XPubSpec -> [XPubBal] -> IO Word32
+ Haskoin.Store.Common: [xPubTxs] :: StoreRead -> XPubSpec -> [XPubBal] -> Limits -> IO [TxRef]
+ Haskoin.Store.Common: [xPubUnspents] :: StoreRead -> XPubSpec -> [XPubBal] -> Limits -> IO [XPubUnspent]
+ Haskoin.Store.Common: data StoreRead
+ Haskoin.Store.Common: notDeleted :: Maybe TxData -> Maybe TxData
+ Haskoin.Store.Common: scanXPubBals :: Monad m => ([Address] -> m [Balance]) -> Word32 -> Word32 -> [(Word32, Address)] -> [(Word32, Address)] -> m [XPubBal]
+ Haskoin.Store.Common: txRefKey :: TxRef -> (BlockRef, Down TxHash)
+ Haskoin.Store.Common: unspentKey :: Unspent -> (BlockRef, Down ByteString)
+ Haskoin.Store.Database.Mempool: AddressMempool :: ![TxRef] -> !HashMap OutPoint Word64 -> ![Unspent] -> AddressMempool
+ Haskoin.Store.Database.Mempool: MemRecords :: !TxHash -> !UnixTime -> !TxData -> ![(OutPoint, Spender)] -> ![(MemAddrKey, MemAddrVal)] -> MemRecords
+ Haskoin.Store.Database.Mempool: [addresses] :: MemRecords -> ![(MemAddrKey, MemAddrVal)]
+ Haskoin.Store.Database.Mempool: [body] :: MemRecords -> !TxData
+ Haskoin.Store.Database.Mempool: [created] :: AddressMempool -> ![Unspent]
+ Haskoin.Store.Database.Mempool: [spends] :: MemRecords -> ![(OutPoint, Spender)]
+ Haskoin.Store.Database.Mempool: [spent] :: AddressMempool -> !HashMap OutPoint Word64
+ Haskoin.Store.Database.Mempool: [time] :: MemRecords -> !UnixTime
+ Haskoin.Store.Database.Mempool: [txid] :: MemRecords -> !TxHash
+ Haskoin.Store.Database.Mempool: [txs] :: AddressMempool -> ![TxRef]
+ Haskoin.Store.Database.Mempool: addressArea :: [MemRecords] -> [(ByteString, ByteString)]
+ Haskoin.Store.Database.Mempool: applyMempool :: Balance -> AddressMempool -> Balance
+ Haskoin.Store.Database.Mempool: applySummary :: Balance -> MemSummary -> Balance
+ Haskoin.Store.Database.Mempool: data AddressMempool
+ Haskoin.Store.Database.Mempool: data MemRecords
+ Haskoin.Store.Database.Mempool: deleteRecordOps :: DB -> MemRecords -> [BatchOp]
+ Haskoin.Store.Database.Mempool: getAddressMempool :: DatabaseReader -> Address -> IO AddressMempool
+ Haskoin.Store.Database.Mempool: getAddressMempools :: DatabaseReader -> [Address] -> IO [AddressMempool]
+ Haskoin.Store.Database.Mempool: getMempoolSpenders :: DatabaseReader -> TxHash -> IO (IntMap Spender)
+ Haskoin.Store.Database.Mempool: getMempoolTx :: DatabaseReader -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Mempool: getSummaries :: DatabaseReader -> [Address] -> IO [Maybe MemSummary]
+ Haskoin.Store.Database.Mempool: hiddenOutputs :: AddressMempool -> HashMap OutPoint Word64
+ Haskoin.Store.Database.Mempool: insertRecordOps :: DB -> MemRecords -> [BatchOp]
+ Haskoin.Store.Database.Mempool: instance GHC.Classes.Eq Haskoin.Store.Database.Mempool.AddressMempool
+ Haskoin.Store.Database.Mempool: instance GHC.Classes.Eq Haskoin.Store.Database.Mempool.MemRecords
+ Haskoin.Store.Database.Mempool: instance GHC.Internal.Show.Show Haskoin.Store.Database.Mempool.AddressMempool
+ Haskoin.Store.Database.Mempool: instance GHC.Internal.Show.Show Haskoin.Store.Database.Mempool.MemRecords
+ Haskoin.Store.Database.Mempool: mempoolRecords :: Network -> Ctx -> UnixTime -> TxData -> MemRecords
+ Haskoin.Store.Database.Mempool: mempoolUnspents :: AddressMempool -> [Unspent]
+ Haskoin.Store.Database.Mempool: mergeBalance :: Address -> Maybe Balance -> AddressMempool -> Maybe Balance
+ Haskoin.Store.Database.Mempool: rebuildSummaries :: DatabaseReader -> IO Int
+ Haskoin.Store.Database.Mempool: recordEntries :: MemRecords -> ([(ByteString, ByteString)], [(ByteString, ByteString)])
+ Haskoin.Store.Database.Mempool: summarise :: AddressMempool -> MemSummary
+ Haskoin.Store.Database.Mempool: summaryBalance :: Address -> Maybe Balance -> Maybe MemSummary -> Maybe Balance
+ Haskoin.Store.Database.Mempool: summaryEntries :: [(MemAddrKey, MemAddrVal)] -> [(MemAddrKey, MemAddrVal)]
+ Haskoin.Store.Database.Mempool: wipeMempoolArea :: DatabaseReader -> IO Int
+ Haskoin.Store.Database.Mempool: wrongSummaries :: DatabaseReader -> IO [(MemAddrKey, Maybe MemAddrVal, Maybe MemAddrVal)]
+ Haskoin.Store.Database.Migrate: Inconsistent :: !Text -> MigrationError
+ Haskoin.Store.Database.Migrate: Migration :: !Int -> !Int -> !Int -> !Int -> Migration
+ Haskoin.Store.Database.Migrate: MigrationOptions :: !Int -> (Int -> IO ()) -> (Text -> IO ()) -> MigrationOptions
+ Haskoin.Store.Database.Migrate: UnknownVersion :: !Maybe Word32 -> MigrationError
+ Haskoin.Store.Database.Migrate: [afterBatch] :: MigrationOptions -> Int -> IO ()
+ Haskoin.Store.Database.Migrate: [batches] :: Migration -> !Int
+ Haskoin.Store.Database.Migrate: [checked] :: Migration -> !Int
+ Haskoin.Store.Database.Migrate: [chunk] :: MigrationOptions -> !Int
+ Haskoin.Store.Database.Migrate: [moved] :: Migration -> !Int
+ Haskoin.Store.Database.Migrate: [report] :: MigrationOptions -> Text -> IO ()
+ Haskoin.Store.Database.Migrate: [rewritten] :: Migration -> !Int
+ Haskoin.Store.Database.Migrate: currentVersion :: Word32
+ Haskoin.Store.Database.Migrate: data Migration
+ Haskoin.Store.Database.Migrate: data MigrationError
+ Haskoin.Store.Database.Migrate: data MigrationOptions
+ Haskoin.Store.Database.Migrate: defaultMigrationOptions :: MigrationOptions
+ Haskoin.Store.Database.Migrate: instance GHC.Classes.Eq Haskoin.Store.Database.Migrate.Migration
+ Haskoin.Store.Database.Migrate: instance GHC.Classes.Eq Haskoin.Store.Database.Migrate.MigrationError
+ Haskoin.Store.Database.Migrate: instance GHC.Internal.Exception.Type.Exception Haskoin.Store.Database.Migrate.MigrationError
+ Haskoin.Store.Database.Migrate: instance GHC.Internal.Show.Show Haskoin.Store.Database.Migrate.Migration
+ Haskoin.Store.Database.Migrate: instance GHC.Internal.Show.Show Haskoin.Store.Database.Migrate.MigrationError
+ Haskoin.Store.Database.Migrate: layeredVersion :: Word32
+ Haskoin.Store.Database.Migrate: migrateLayered :: DatabaseReader -> IO Migration
+ Haskoin.Store.Database.Migrate: migrateLayeredWith :: MigrationOptions -> DatabaseReader -> IO Migration
+ Haskoin.Store.Database.Migrate: migrateRecords :: (Text -> IO ()) -> DatabaseReader -> IO Int
+ Haskoin.Store.Database.Migrate: upgradeDatabase :: (Text -> IO ()) -> DatabaseReader -> IO Migration
+ Haskoin.Store.Database.Query: count :: DatabaseReader -> (DataMetrics -> IO ()) -> IO ()
+ Haskoin.Store.Database.Query: databaseRead :: DatabaseReader -> StoreRead
+ Haskoin.Store.Database.Query: getAddressTxs :: DatabaseReader -> Address -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query: getAddressUnspents :: DatabaseReader -> Address -> Limits -> IO [Unspent]
+ Haskoin.Store.Database.Query: getAddressesTxCount :: DatabaseReader -> [Address] -> IO Word32
+ Haskoin.Store.Database.Query: getAddressesTxs :: DatabaseReader -> [Address] -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query: getAddressesUnspents :: DatabaseReader -> [Address] -> Limits -> IO [Unspent]
+ Haskoin.Store.Database.Query: getBalance :: DatabaseReader -> Address -> IO (Maybe Balance)
+ Haskoin.Store.Database.Query: getBalances :: DatabaseReader -> [Address] -> IO [Balance]
+ Haskoin.Store.Database.Query: getBestBlock :: DatabaseReader -> IO (Maybe BlockHash)
+ Haskoin.Store.Database.Query: getBlock :: DatabaseReader -> BlockHash -> IO (Maybe BlockData)
+ Haskoin.Store.Database.Query: getBlocksAtHeight :: DatabaseReader -> BlockHeight -> IO [BlockHash]
+ Haskoin.Store.Database.Query: getChainSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Query: getChainSpenders :: DatabaseReader -> TxHash -> IO (IntMap Spender)
+ Haskoin.Store.Database.Query: getManyTxData :: DatabaseReader -> [TxHash] -> IO [Maybe TxData]
+ Haskoin.Store.Database.Query: getMempool :: DatabaseReader -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query: getMempoolBefore :: DatabaseReader -> UnixTime -> Int -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query: getMempoolPage :: DatabaseReader -> Limits -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query: getNumTxData :: DatabaseReader -> Word64 -> IO [TxData]
+ Haskoin.Store.Database.Query: getSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Query: getTxData :: DatabaseReader -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Query: getTxRecord :: DatabaseReader -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Query: getUnspent :: DatabaseReader -> OutPoint -> IO (Maybe Unspent)
+ Haskoin.Store.Database.Query: joinConduits :: Ord k => (o -> k) -> [ConduitT () o IO ()] -> Limits -> IO [o]
+ Haskoin.Store.Database.Query: retrieveManyCF :: (Serialize key, Serialize value) => DB -> ColumnFamily -> [key] -> IO [Maybe value]
+ Haskoin.Store.Database.Query: usedAddresses :: [XPubBal] -> [Address]
+ Haskoin.Store.Database.Query: withChainSpenders :: DatabaseReader -> [(TxHash, TxData)] -> IO [TxData]
+ Haskoin.Store.Database.Query: withManyIters :: Serialize k => DatabaseReader -> ColumnFamily -> [k] -> ([Iterator] -> IO a) -> IO a
+ Haskoin.Store.Database.Query: xPubBals :: DatabaseReader -> XPubSpec -> IO [XPubBal]
+ Haskoin.Store.Database.Query: xPubTxCount :: DatabaseReader -> [XPubBal] -> IO Word32
+ Haskoin.Store.Database.Query: xPubTxs :: DatabaseReader -> [XPubBal] -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query: xPubUnspents :: DatabaseReader -> [XPubBal] -> Limits -> IO [XPubUnspent]
+ Haskoin.Store.Database.Query.Layered: InChain :: !BlockRef -> From
+ Haskoin.Store.Database.Query.Layered: InMempool :: !UnixTime -> !TxHash -> From
+ Haskoin.Store.Database.Query.Layered: Top :: From
+ Haskoin.Store.Database.Query.Layered: chainTxsC :: From -> Address -> Iterator -> ConduitT i TxRef IO ()
+ Haskoin.Store.Database.Query.Layered: chainUnspentsC :: DatabaseReader -> From -> Address -> Iterator -> ConduitT i Unspent IO ()
+ Haskoin.Store.Database.Query.Layered: data From
+ Haskoin.Store.Database.Query.Layered: data View
+ Haskoin.Store.Database.Query.Layered: getAddressTxs :: View -> Address -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query.Layered: getAddressUnspents :: View -> Address -> Limits -> IO [Unspent]
+ Haskoin.Store.Database.Query.Layered: getAddressesTxCount :: View -> [Address] -> IO Word32
+ Haskoin.Store.Database.Query.Layered: getAddressesTxs :: View -> [Address] -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query.Layered: getAddressesUnspents :: View -> [Address] -> Limits -> IO [Unspent]
+ Haskoin.Store.Database.Query.Layered: getBalance :: View -> Address -> IO (Maybe Balance)
+ Haskoin.Store.Database.Query.Layered: getBalances :: View -> [Address] -> IO [Balance]
+ Haskoin.Store.Database.Query.Layered: getBestBlock :: View -> IO (Maybe BlockHash)
+ Haskoin.Store.Database.Query.Layered: getBlock :: View -> BlockHash -> IO (Maybe BlockData)
+ Haskoin.Store.Database.Query.Layered: getBlocksAtHeight :: View -> BlockHeight -> IO [BlockHash]
+ Haskoin.Store.Database.Query.Layered: getManyTxData :: View -> [TxHash] -> IO [Maybe TxData]
+ Haskoin.Store.Database.Query.Layered: getMempool :: View -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query.Layered: getMempoolPage :: View -> Limits -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query.Layered: getNumTxData :: View -> Word64 -> IO [TxData]
+ Haskoin.Store.Database.Query.Layered: getSpender :: View -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Query.Layered: getTxData :: View -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Query.Layered: getUnspent :: View -> OutPoint -> IO (Maybe Unspent)
+ Haskoin.Store.Database.Query.Layered: layeredRead :: View -> StoreRead
+ Haskoin.Store.Database.Query.Layered: standaloneRead :: DatabaseReader -> StoreRead
+ Haskoin.Store.Database.Query.Layered: viewReader :: View -> DatabaseReader
+ Haskoin.Store.Database.Query.Layered: withSpenders :: HashMap TxHash (IntMap Spender) -> TxHash -> TxData -> TxData
+ Haskoin.Store.Database.Query.Layered: withView :: DatabaseReader -> (View -> IO a) -> IO a
+ Haskoin.Store.Database.Query.Layered: xPubBals :: View -> XPubSpec -> IO [XPubBal]
+ Haskoin.Store.Database.Query.Layered: xPubTxCount :: View -> [XPubBal] -> IO Word32
+ Haskoin.Store.Database.Query.Layered: xPubTxs :: View -> [XPubBal] -> Limits -> IO [TxRef]
+ Haskoin.Store.Database.Query.Layered: xPubUnspents :: View -> [XPubBal] -> Limits -> IO [XPubUnspent]
+ Haskoin.Store.Database.Query.Pooled: PoolReads :: ([Address] -> IO [Maybe MemSummary]) -> (Address -> Maybe (UnixTime, TxHash) -> Int -> IO [(UnixTime, TxHash)]) -> (Address -> Maybe (UnixTime, OutPoint) -> Int -> IO [(Unspent, Bool)]) -> (Address -> [OutPoint] -> IO [Bool]) -> ([TxHash] -> IO [(Maybe TxData, IntMap Spender)]) -> ([OutPoint] -> IO [Maybe Spender]) -> (Word32 -> Word32 -> IO [(UnixTime, TxHash)]) -> ((Word32, Word16) -> IO [TxHash]) -> PoolReads
+ Haskoin.Store.Database.Query.Pooled: [addressOuts] :: PoolReads -> Address -> Maybe (UnixTime, OutPoint) -> Int -> IO [(Unspent, Bool)]
+ Haskoin.Store.Database.Query.Pooled: [addressTxs] :: PoolReads -> Address -> Maybe (UnixTime, TxHash) -> Int -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query.Pooled: [bodies] :: PoolReads -> [TxHash] -> IO [(Maybe TxData, IntMap Spender)]
+ Haskoin.Store.Database.Query.Pooled: [page] :: PoolReads -> Word32 -> Word32 -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Query.Pooled: [spenders] :: PoolReads -> [OutPoint] -> IO [Maybe Spender]
+ Haskoin.Store.Database.Query.Pooled: [spentOutputs] :: PoolReads -> Address -> [OutPoint] -> IO [Bool]
+ Haskoin.Store.Database.Query.Pooled: [startingWith] :: PoolReads -> (Word32, Word16) -> IO [TxHash]
+ Haskoin.Store.Database.Query.Pooled: [summaries] :: PoolReads -> [Address] -> IO [Maybe MemSummary]
+ Haskoin.Store.Database.Query.Pooled: data PoolReads
+ Haskoin.Store.Database.Query.Pooled: localPool :: DatabaseReader -> PoolReads
+ Haskoin.Store.Database.Query.Pooled: pageStart :: DatabaseReader -> PoolReads -> Maybe Start -> IO (Maybe From)
+ Haskoin.Store.Database.Query.Pooled: pooledRead :: DatabaseReader -> PoolReads -> StoreRead
+ Haskoin.Store.Database.Reader: DataMetrics :: !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatGauge -> DataMetrics
+ Haskoin.Store.Database.Reader: Derived :: ![(Word32, Address)] -> ![(Word32, Address)] -> Derived
+ Haskoin.Store.Database.Reader: [dataAddrTxCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataBalanceCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataBestCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataBlockCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataIters] :: DataMetrics -> !StatGauge
+ Haskoin.Store.Database.Reader: [dataMempoolCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataTxCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataUnspentCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataXPubBals] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataXPubTxCount] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataXPubTxs] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: [dataXPubUnspents] :: DataMetrics -> !StatCounter
+ Haskoin.Store.Database.Reader: data Derived
+ Haskoin.Store.Database.Reader: derivedAddrs :: DatabaseReader -> XPubSpec -> IO Derived
+ Haskoin.Store.Database.Reader: initRocksDB :: DatabaseReader -> IO ()
+ Haskoin.Store.Database.Reader: mempoolAddrCF :: DB -> ColumnFamily
+ Haskoin.Store.Database.Reader: spenderCF :: DB -> ColumnFamily
+ Haskoin.Store.Database.Reader: withLayeredReader :: Network -> Ctx -> Word32 -> Word32 -> FilePath -> Bool -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe DataMetrics -> (DatabaseReader -> IO a) -> IO a
+ Haskoin.Store.Database.Types: MemAddrKeyA :: !Address -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrKeyS :: MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrOutKey :: !Address -> !UnixTime -> !OutPoint -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrOutKeyA :: !Address -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrOutVal :: !Word64 -> !ByteString -> MemAddrVal
+ Haskoin.Store.Database.Types: MemAddrSpentKey :: !Address -> !OutPoint -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrSpentVal :: !Word64 -> MemAddrVal
+ Haskoin.Store.Database.Types: MemAddrSumKey :: !Address -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrSumVal :: !MemSummary -> MemAddrVal
+ Haskoin.Store.Database.Types: MemAddrTxKey :: !Address -> !UnixTime -> !TxHash -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrTxKeyA :: !Address -> MemAddrKey
+ Haskoin.Store.Database.Types: MemAddrTxVal :: MemAddrVal
+ Haskoin.Store.Database.Types: MemDeletedKey :: !TxHash -> MemDeletedKey
+ Haskoin.Store.Database.Types: MemDeletedKeyP :: !(Word32, Word16) -> MemDeletedKey
+ Haskoin.Store.Database.Types: MemDeletedKeyS :: MemDeletedKey
+ Haskoin.Store.Database.Types: MemSpenderKey :: !OutPoint -> MemSpenderKey
+ Haskoin.Store.Database.Types: MemSpenderKeyB :: MemSpenderKey
+ Haskoin.Store.Database.Types: MemSpenderKeyS :: !TxHash -> MemSpenderKey
+ Haskoin.Store.Database.Types: MemSummary :: !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> MemSummary
+ Haskoin.Store.Database.Types: MigrationKey :: MigrationKey
+ Haskoin.Store.Database.Types: SpenderKey :: !TxHash -> !Word32 -> SpenderKey
+ Haskoin.Store.Database.Types: SpenderKeyS :: !TxHash -> SpenderKey
+ Haskoin.Store.Database.Types: SpenderVal :: IntMap Spender -> SpenderVal
+ Haskoin.Store.Database.Types: [bothCount] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: [both] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: [paidCount] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: [paid] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: [spentCount] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: [spent] :: MemSummary -> !Word64
+ Haskoin.Store.Database.Types: data MemAddrKey
+ Haskoin.Store.Database.Types: data MemAddrVal
+ Haskoin.Store.Database.Types: data MemDeletedKey
+ Haskoin.Store.Database.Types: data MemSpenderKey
+ Haskoin.Store.Database.Types: data MemSummary
+ Haskoin.Store.Database.Types: data MigrationKey
+ Haskoin.Store.Database.Types: data SpenderKey
+ Haskoin.Store.Database.Types: instance Control.DeepSeq.NFData Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance Control.DeepSeq.NFData Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance Data.Hashable.Class.Hashable Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance Data.Serialize.Serialize Haskoin.Store.Database.Types.SpenderVal
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.Key Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.Key Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.Key Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.Key Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.Key Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.KeyValue Haskoin.Store.Database.Types.MemAddrKey Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.KeyValue Haskoin.Store.Database.Types.MemDeletedKey Haskoin.Store.Data.TxData
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.KeyValue Haskoin.Store.Database.Types.MemSpenderKey Haskoin.Store.Data.Spender
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.KeyValue Haskoin.Store.Database.Types.MigrationKey GHC.Internal.Word.Word32
+ Haskoin.Store.Database.Types: instance Database.RocksDB.Query.KeyValue Haskoin.Store.Database.Types.SpenderKey Haskoin.Store.Database.Types.SpenderVal
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Eq Haskoin.Store.Database.Types.SpenderVal
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance GHC.Classes.Ord Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Generics.Generic Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Read.Read Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MemAddrKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MemAddrVal
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MemDeletedKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MemSpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MemSummary
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.MigrationKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.SpenderKey
+ Haskoin.Store.Database.Types: instance GHC.Internal.Show.Show Haskoin.Store.Database.Types.SpenderVal
+ Haskoin.Store.Database.Types: newtype SpenderVal
+ Haskoin.Store.Database.Types: spenderGroup :: Word32
+ Haskoin.Store.Database.Types: spenderKey :: OutPoint -> SpenderKey
+ Haskoin.Store.Database.Views: closeViews :: Views b -> IO ()
+ Haskoin.Store.Database.Views: data Views b
+ Haskoin.Store.Database.Views: heldViews :: Views b -> IO [b]
+ Haskoin.Store.Database.Views: keepViews :: Ord b => Views b -> [b] -> IO ()
+ Haskoin.Store.Database.Views: newViews :: DatabaseReader -> IO (Views b)
+ Haskoin.Store.Database.Views: takeView :: Ord b => Views b -> b -> IO ()
+ Haskoin.Store.Database.Views: withViewAt :: Ord b => Views b -> b -> (DatabaseReader -> IO a) -> IO (Maybe a)
+ Haskoin.Store.Database.Writer: addToMempool :: Writer -> TxHash -> UnixTime -> IO ()
+ Haskoin.Store.Database.Writer: data Writer
+ Haskoin.Store.Database.Writer: deleteAddrTx :: Writer -> Address -> TxRef -> IO ()
+ Haskoin.Store.Database.Writer: deleteAddrUnspent :: Writer -> Address -> Unspent -> IO ()
+ Haskoin.Store.Database.Writer: deleteBalance :: Writer -> Address -> IO ()
+ Haskoin.Store.Database.Writer: deleteFromMempool :: Writer -> TxHash -> IO ()
+ Haskoin.Store.Database.Writer: deleteMempoolAddr :: Writer -> MemAddrKey -> IO ()
+ Haskoin.Store.Database.Writer: deleteMempoolSpender :: Writer -> OutPoint -> IO ()
+ Haskoin.Store.Database.Writer: deleteMempoolTx :: Writer -> TxHash -> IO ()
+ Haskoin.Store.Database.Writer: deleteSpender :: Writer -> OutPoint -> IO ()
+ Haskoin.Store.Database.Writer: deleteTxData :: Writer -> TxHash -> IO ()
+ Haskoin.Store.Database.Writer: deleteUnspent :: Writer -> OutPoint -> IO ()
+ Haskoin.Store.Database.Writer: getBalance :: Writer -> Address -> IO (Maybe Balance)
+ Haskoin.Store.Database.Writer: getBestBlock :: Writer -> IO (Maybe BlockHash)
+ Haskoin.Store.Database.Writer: getBlock :: Writer -> BlockHash -> IO (Maybe BlockData)
+ Haskoin.Store.Database.Writer: getBlocksAtHeight :: Writer -> BlockHeight -> IO [BlockHash]
+ Haskoin.Store.Database.Writer: getChainSpender :: Writer -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Writer: getChainSpenders :: Writer -> TxHash -> Int -> IO (IntMap Spender)
+ Haskoin.Store.Database.Writer: getMempool :: Writer -> IO [(UnixTime, TxHash)]
+ Haskoin.Store.Database.Writer: getMempoolAddr :: Writer -> MemAddrKey -> IO (Maybe MemAddrVal)
+ Haskoin.Store.Database.Writer: getMempoolSpender :: Writer -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Writer: getMempoolSpenders :: Writer -> TxHash -> Int -> IO (IntMap Spender)
+ Haskoin.Store.Database.Writer: getMempoolTx :: Writer -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Writer: getSpender :: Writer -> OutPoint -> IO (Maybe Spender)
+ Haskoin.Store.Database.Writer: getTxData :: Writer -> TxHash -> IO (Maybe TxData)
+ Haskoin.Store.Database.Writer: getUnspent :: Writer -> OutPoint -> IO (Maybe Unspent)
+ Haskoin.Store.Database.Writer: inMempool :: Writer -> TxHash -> IO Bool
+ Haskoin.Store.Database.Writer: insertAddrTx :: Writer -> Address -> TxRef -> IO ()
+ Haskoin.Store.Database.Writer: insertAddrUnspent :: Writer -> Address -> Unspent -> IO ()
+ Haskoin.Store.Database.Writer: insertBlock :: Writer -> BlockData -> IO ()
+ Haskoin.Store.Database.Writer: insertMempoolAddr :: Writer -> MemAddrKey -> MemAddrVal -> IO ()
+ Haskoin.Store.Database.Writer: insertMempoolSpender :: Writer -> OutPoint -> Spender -> IO ()
+ Haskoin.Store.Database.Writer: insertMempoolTx :: Writer -> TxData -> IO ()
+ Haskoin.Store.Database.Writer: insertSpender :: Writer -> OutPoint -> Spender -> IO ()
+ Haskoin.Store.Database.Writer: insertTx :: Writer -> TxData -> IO ()
+ Haskoin.Store.Database.Writer: insertUnspent :: Writer -> Unspent -> IO ()
+ Haskoin.Store.Database.Writer: setBalance :: Writer -> Balance -> IO ()
+ Haskoin.Store.Database.Writer: setBest :: Writer -> BlockHash -> IO ()
+ Haskoin.Store.Database.Writer: setBlocksAtHeight :: Writer -> [BlockHash] -> BlockHeight -> IO ()
+ Haskoin.Store.Database.Writer: writerCtx :: Writer -> Ctx
+ Haskoin.Store.Database.Writer: writerNet :: Writer -> Network
+ Haskoin.Store.Fleet: Alone :: Answer b
+ Haskoin.Store.Fleet: At :: !b -> Answer b
+ Haskoin.Store.Fleet: Behind :: Answer b
+ Haskoin.Store.Fleet: Fleet :: !Maybe b -> !Map s (Report b) -> Fleet s b
+ Haskoin.Store.Fleet: Moved :: !Fleet s b -> Move s b
+ Haskoin.Store.Fleet: Raced :: Move s b
+ Haskoin.Store.Fleet: Report :: ![b] -> !Time -> Report b
+ Haskoin.Store.Fleet: Stays :: Move s b
+ Haskoin.Store.Fleet: [expires] :: Report b -> !Time
+ Haskoin.Store.Fleet: [recent] :: Report b -> ![b]
+ Haskoin.Store.Fleet: [reports] :: Fleet s b -> !Map s (Report b)
+ Haskoin.Store.Fleet: [tip] :: Fleet s b -> !Maybe b
+ Haskoin.Store.Fleet: agreed :: Eq b => Time -> Fleet s b -> Maybe b
+ Haskoin.Store.Fleet: answer :: (b -> Bool) -> Fleet s b -> Answer b
+ Haskoin.Store.Fleet: data Answer b
+ Haskoin.Store.Fleet: data Fleet s b
+ Haskoin.Store.Fleet: data Move s b
+ Haskoin.Store.Fleet: data Report b
+ Haskoin.Store.Fleet: empty :: Fleet s b
+ Haskoin.Store.Fleet: instance (GHC.Classes.Eq b, GHC.Classes.Eq s) => GHC.Classes.Eq (Haskoin.Store.Fleet.Fleet s b)
+ Haskoin.Store.Fleet: instance (GHC.Classes.Eq b, GHC.Classes.Eq s) => GHC.Classes.Eq (Haskoin.Store.Fleet.Move s b)
+ Haskoin.Store.Fleet: instance (GHC.Internal.Show.Show b, GHC.Internal.Show.Show s) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Fleet s b)
+ Haskoin.Store.Fleet: instance (GHC.Internal.Show.Show b, GHC.Internal.Show.Show s) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Move s b)
+ Haskoin.Store.Fleet: instance GHC.Classes.Eq b => GHC.Classes.Eq (Haskoin.Store.Fleet.Answer b)
+ Haskoin.Store.Fleet: instance GHC.Classes.Eq b => GHC.Classes.Eq (Haskoin.Store.Fleet.Report b)
+ Haskoin.Store.Fleet: instance GHC.Internal.Show.Show b => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Answer b)
+ Haskoin.Store.Fleet: instance GHC.Internal.Show.Show b => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Report b)
+ Haskoin.Store.Fleet: keep :: [b] -> Maybe b -> [b]
+ Haskoin.Store.Fleet: live :: Time -> Fleet s b -> Map s (Report b)
+ Haskoin.Store.Fleet: mayReport :: Ord s => Time -> s -> (b -> Bool) -> Fleet s b -> Bool
+ Haskoin.Store.Fleet: move :: Eq b => Time -> Maybe b -> Fleet s b -> Move s b
+ Haskoin.Store.Fleet: report :: Ord s => Time -> Time -> s -> [b] -> Fleet s b -> Fleet s b
+ Haskoin.Store.Fleet: type Time = Word64
+ Haskoin.Store.Fleet.Member: Alone :: Standing a
+ Haskoin.Store.Fleet.Member: Answered :: !a -> Standing a
+ Haskoin.Store.Fleet.Member: Behind :: Standing a
+ Haskoin.Store.Fleet.Member: Between :: !PoolChange -> ![TxData] -> !Bool -> ![BlockHash] -> Between
+ Haskoin.Store.Fleet.Member: CacheMetrics :: !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatCounter -> !StatTiming -> !StatGauge -> !StatGauge -> CacheMetrics
+ Haskoin.Store.Fleet.Member: Member :: !MemberConfig -> !Connection -> !FleetKeys -> !DatabaseReader -> !Views BlockHash -> !TVar [BlockHash] -> !TVar [BlockHash] -> !TVar (Maybe BlockHash) -> !TVar (HashSet TxHash) -> !TVar (HashMap TxHash UnixTime) -> !TVar Int -> !TVar (Maybe BlockHash) -> !TVar Bool -> !TVar Bool -> !TVar (HashMap ByteString XPubSpec) -> !TVar (Maybe ByteString) -> !Maybe CacheMetrics -> !TVar (HashSet TxHash) -> Member
+ Haskoin.Store.Fleet.Member: MemberConfig :: !ByteString -> !ByteString -> !Word64 -> !Int -> !Word64 -> !Int -> !Int -> !Int -> !Maybe Stats -> !LogLevel -> Text -> IO () -> MemberConfig
+ Haskoin.Store.Fleet.Member: TipMoved :: TipMoved
+ Haskoin.Store.Fleet.Member: [abandoned] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [added] :: Between -> ![TxData]
+ Haskoin.Store.Fleet.Member: [addresses] :: CacheMetrics -> !StatGauge
+ Haskoin.Store.Fleet.Member: [buildTime] :: CacheMetrics -> !StatTiming
+ Haskoin.Store.Fleet.Member: [builds] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [cacheMax] :: MemberConfig -> !Int
+ Haskoin.Store.Fleet.Member: [cacheMinAddrs] :: MemberConfig -> !Int
+ Haskoin.Store.Fleet.Member: [cacheMinTxs] :: MemberConfig -> !Int
+ Haskoin.Store.Fleet.Member: [changed] :: Member -> !TVar Bool
+ Haskoin.Store.Fleet.Member: [config] :: Member -> !MemberConfig
+ Haskoin.Store.Fleet.Member: [db] :: Member -> !DatabaseReader
+ Haskoin.Store.Fleet.Member: [depth] :: MemberConfig -> !Int
+ Haskoin.Store.Fleet.Member: [emptied] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [evictions] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [expiry] :: MemberConfig -> !Word64
+ Haskoin.Store.Fleet.Member: [gone] :: Member -> !TVar (HashSet TxHash)
+ Haskoin.Store.Fleet.Member: [healthy] :: Member -> !TVar Bool
+ Haskoin.Store.Fleet.Member: [hits] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [keys] :: Member -> !FleetKeys
+ Haskoin.Store.Fleet.Member: [lagging] :: Member -> !TVar (Maybe ByteString)
+ Haskoin.Store.Fleet.Member: [lease] :: MemberConfig -> !Word64
+ Haskoin.Store.Fleet.Member: [metrics] :: Member -> !Maybe CacheMetrics
+ Haskoin.Store.Fleet.Member: [misses] :: CacheMetrics -> !StatCounter
+ Haskoin.Store.Fleet.Member: [name] :: MemberConfig -> !ByteString
+ Haskoin.Store.Fleet.Member: [note] :: MemberConfig -> !LogLevel -> Text -> IO ()
+ Haskoin.Store.Fleet.Member: [offeredAt] :: Member -> !TVar (Maybe BlockHash)
+ Haskoin.Store.Fleet.Member: [offered] :: Member -> !TVar (HashSet TxHash)
+ Haskoin.Store.Fleet.Member: [path] :: Between -> ![BlockHash]
+ Haskoin.Store.Fleet.Member: [pool] :: Between -> !PoolChange
+ Haskoin.Store.Fleet.Member: [prefix] :: MemberConfig -> !ByteString
+ Haskoin.Store.Fleet.Member: [recent] :: Member -> !TVar [BlockHash]
+ Haskoin.Store.Fleet.Member: [redis] :: Member -> !Connection
+ Haskoin.Store.Fleet.Member: [reorganised] :: Between -> !Bool
+ Haskoin.Store.Fleet.Member: [reported] :: Member -> !TVar [BlockHash]
+ Haskoin.Store.Fleet.Member: [rounds] :: Member -> !TVar Int
+ Haskoin.Store.Fleet.Member: [stats] :: MemberConfig -> !Maybe Stats
+ Haskoin.Store.Fleet.Member: [tip] :: Member -> !TVar (Maybe BlockHash)
+ Haskoin.Store.Fleet.Member: [views] :: Member -> !Views BlockHash
+ Haskoin.Store.Fleet.Member: [waiting] :: Member -> !TVar (HashMap TxHash UnixTime)
+ Haskoin.Store.Fleet.Member: [wanted] :: Member -> !TVar (HashMap ByteString XPubSpec)
+ Haskoin.Store.Fleet.Member: [xpubs] :: CacheMetrics -> !StatGauge
+ Haskoin.Store.Fleet.Member: answering :: Member -> (DatabaseReader -> PoolReads -> IO a) -> IO (Standing a)
+ Haskoin.Store.Fleet.Member: answeringAt :: Member -> ByteString -> (DatabaseReader -> PoolReads -> IO () -> IO a) -> IO (Maybe a)
+ Haskoin.Store.Fleet.Member: answeringCached :: Member -> ByteString -> (StoreRead -> StoreRead -> IO () -> IO a) -> IO (Maybe a)
+ Haskoin.Store.Fleet.Member: blocksBetween :: DatabaseReader -> Maybe BlockHash -> BlockHash -> IO (Maybe Between)
+ Haskoin.Store.Fleet.Member: changeBetween :: DatabaseReader -> Maybe BlockHash -> BlockHash -> IO (Maybe PoolChange)
+ Haskoin.Store.Fleet.Member: data Between
+ Haskoin.Store.Fleet.Member: data CacheMetrics
+ Haskoin.Store.Fleet.Member: data Member
+ Haskoin.Store.Fleet.Member: data MemberConfig
+ Haskoin.Store.Fleet.Member: data Standing a
+ Haskoin.Store.Fleet.Member: data TipMoved
+ Haskoin.Store.Fleet.Member: expirePool :: Member -> IO ()
+ Haskoin.Store.Fleet.Member: instance GHC.Internal.Exception.Type.Exception Haskoin.Store.Fleet.Member.TipMoved
+ Haskoin.Store.Fleet.Member: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Member.TipMoved
+ Haskoin.Store.Fleet.Member: memberAlive :: Member -> IO Bool
+ Haskoin.Store.Fleet.Member: memberCacheLoop :: Member -> (String -> IO ()) -> IO a
+ Haskoin.Store.Fleet.Member: memberEvents :: Member -> Bool -> (FleetEvent -> IO ()) -> (String -> IO ()) -> IO a
+ Haskoin.Store.Fleet.Member: memberForget :: Member -> [TxHash] -> IO ()
+ Haskoin.Store.Fleet.Member: memberLoop :: Member -> IO () -> (String -> IO ()) -> IO a
+ Haskoin.Store.Fleet.Member: memberTick :: Member -> IO ()
+ Haskoin.Store.Fleet.Member: memberTip :: Member -> IO (Maybe ByteString)
+ Haskoin.Store.Fleet.Member: moveTip :: Member -> IO Bool
+ Haskoin.Store.Fleet.Member: newMember :: MemberConfig -> Connection -> DatabaseReader -> IO Member
+ Haskoin.Store.Fleet.Member: noteBest :: Member -> BlockHash -> IO ()
+ Haskoin.Store.Fleet.Member: offerMempool :: Member -> IO ()
+ Haskoin.Store.Fleet.Member: poolTx :: Network -> Ctx -> TxData -> PoolTx
+ Haskoin.Store.Fleet.Member: redisPool :: Connection -> FleetKeys -> ByteString -> IO () -> PoolReads
+ Haskoin.Store.Fleet.Member: report :: Member -> IO ()
+ Haskoin.Store.Fleet.Member: syncCache :: Member -> IO ()
+ Haskoin.Store.Fleet.Member: wantCached :: Member -> XPubSpec -> [XPubBal] -> IO ()
+ Haskoin.Store.Fleet.Pool: AddressPool :: ![(Word64, t)] -> ![(Word64, o, Word64)] -> !Map o Word64 -> AddressPool t o
+ Haskoin.Store.Fleet.Pool: Change :: ![(t, [o])] -> ![t] -> ![t] -> ![o] -> Change t o
+ Haskoin.Store.Fleet.Pool: Conflict :: ![t] -> Refusal t o
+ Haskoin.Store.Fleet.Pool: Known :: Refusal t o
+ Haskoin.Store.Fleet.Pool: Orphan :: ![o] -> Refusal t o
+ Haskoin.Store.Fleet.Pool: Pays :: !a -> !o -> !Word64 -> Entry a o
+ Haskoin.Store.Fleet.Pool: Pool :: !Map t [o] -> !Map o t -> !Map t (Set t) -> Pool t o
+ Haskoin.Store.Fleet.Pool: Refused :: !Refusal t o -> Offer t o
+ Haskoin.Store.Fleet.Pool: Spends :: !a -> !o -> !Word64 -> Entry a o
+ Haskoin.Store.Fleet.Pool: Taken :: !Pool t o -> Offer t o
+ Haskoin.Store.Fleet.Pool: Totals :: !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> Totals
+ Haskoin.Store.Fleet.Pool: Touches :: !a -> Entry a o
+ Haskoin.Store.Fleet.Pool: [bothCount] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [both] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [children] :: Pool t o -> !Map t (Set t)
+ Haskoin.Store.Fleet.Pool: [confirmed] :: Change t o -> ![t]
+ Haskoin.Store.Fleet.Pool: [gone] :: Change t o -> ![t]
+ Haskoin.Store.Fleet.Pool: [paidCount] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [paid] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [returned] :: Change t o -> ![(t, [o])]
+ Haskoin.Store.Fleet.Pool: [spender] :: Pool t o -> !Map o t
+ Haskoin.Store.Fleet.Pool: [spends] :: Pool t o -> !Map t [o]
+ Haskoin.Store.Fleet.Pool: [spentCount] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [spent] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: [txs] :: Totals -> !Word64
+ Haskoin.Store.Fleet.Pool: addresses :: (Ord a, Ord t, Ord o) => [(t, Word64, [Entry a o])] -> Map a (AddressPool t o)
+ Haskoin.Store.Fleet.Pool: advance :: (Ord t, Ord o) => (o -> t) -> Change t o -> Pool t o -> Pool t o
+ Haskoin.Store.Fleet.Pool: data AddressPool t o
+ Haskoin.Store.Fleet.Pool: data Change t o
+ Haskoin.Store.Fleet.Pool: data Entry a o
+ Haskoin.Store.Fleet.Pool: data Offer t o
+ Haskoin.Store.Fleet.Pool: data Pool t o
+ Haskoin.Store.Fleet.Pool: data Refusal t o
+ Haskoin.Store.Fleet.Pool: data Totals
+ Haskoin.Store.Fleet.Pool: descendants :: Ord t => [t] -> Pool t o -> Set t
+ Haskoin.Store.Fleet.Pool: empty :: Pool t o
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq a, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.Entry a o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq t, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.AddressPool t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq t, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.Change t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq t, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.Offer t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq t, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.Pool t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Classes.Eq t, GHC.Classes.Eq o) => GHC.Classes.Eq (Haskoin.Store.Fleet.Pool.Refusal t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show a, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.Entry a o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show t, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.AddressPool t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show t, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.Change t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show t, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.Offer t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show t, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.Pool t o)
+ Haskoin.Store.Fleet.Pool: instance (GHC.Internal.Show.Show t, GHC.Internal.Show.Show o) => GHC.Internal.Show.Show (Haskoin.Store.Fleet.Pool.Refusal t o)
+ Haskoin.Store.Fleet.Pool: instance GHC.Classes.Eq Haskoin.Store.Fleet.Pool.Totals
+ Haskoin.Store.Fleet.Pool: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Pool.Totals
+ Haskoin.Store.Fleet.Pool: member :: Ord t => t -> Pool t o -> Bool
+ Haskoin.Store.Fleet.Pool: offer :: (Ord t, Ord o) => (o -> t) -> (o -> Bool) -> t -> [o] -> [t] -> Pool t o -> Offer t o
+ Haskoin.Store.Fleet.Pool: replaceable :: Ord t => (t -> Bool) -> (o -> t) -> Pool t o -> Set t
+ Haskoin.Store.Fleet.Pool: totals :: Ord o => AddressPool t o -> Totals
+ Haskoin.Store.Fleet.Pool: withdraw :: (Ord t, Ord o) => [t] -> Pool t o -> Pool t o
+ Haskoin.Store.Fleet.Redis: Conflict :: ![ByteString] -> Offered
+ Haskoin.Store.Fleet.Redis: FleetBlock :: !ByteString -> FleetEvent
+ Haskoin.Store.Fleet.Redis: FleetEntered :: !ByteString -> FleetEvent
+ Haskoin.Store.Fleet.Redis: FleetKeys :: !ByteString -> !ByteString -> !ByteString -> !ByteString -> !Int -> FleetKeys
+ Haskoin.Store.Fleet.Redis: FleetLeft :: !ByteString -> FleetEvent
+ Haskoin.Store.Fleet.Redis: Known :: Offered
+ Haskoin.Store.Fleet.Redis: Late :: Offered
+ Haskoin.Store.Fleet.Redis: MovedTo :: !ByteString -> Moved
+ Haskoin.Store.Fleet.Redis: Orphan :: ![ByteString] -> Offered
+ Haskoin.Store.Fleet.Redis: PoolChange :: ![PoolTx] -> ![ByteString] -> ![ByteString] -> ![ByteString] -> PoolChange
+ Haskoin.Store.Fleet.Redis: PoolPays :: !ByteString -> !ByteString -> !Word64 -> !ByteString -> PoolEntry
+ Haskoin.Store.Fleet.Redis: PoolSpends :: !ByteString -> !ByteString -> !Word64 -> PoolEntry
+ Haskoin.Store.Fleet.Redis: PoolTouches :: !ByteString -> PoolEntry
+ Haskoin.Store.Fleet.Redis: PoolTx :: !ByteString -> !Word64 -> ![ByteString] -> !ByteString -> !Bool -> ![PoolEntry] -> PoolTx
+ Haskoin.Store.Fleet.Redis: Raced :: Moved
+ Haskoin.Store.Fleet.Redis: Stays :: Moved
+ Haskoin.Store.Fleet.Redis: Taken :: Offered
+ Haskoin.Store.Fleet.Redis: [body] :: PoolTx -> !ByteString
+ Haskoin.Store.Fleet.Redis: [confirmed] :: PoolChange -> ![ByteString]
+ Haskoin.Store.Fleet.Redis: [entries] :: PoolTx -> ![PoolEntry]
+ Haskoin.Store.Fleet.Redis: [gone] :: PoolChange -> ![ByteString]
+ Haskoin.Store.Fleet.Redis: [inputs] :: PoolTx -> ![ByteString]
+ Haskoin.Store.Fleet.Redis: [lease] :: FleetKeys -> !ByteString
+ Haskoin.Store.Fleet.Redis: [prefix] :: FleetKeys -> !ByteString
+ Haskoin.Store.Fleet.Redis: [recent] :: FleetKeys -> !ByteString
+ Haskoin.Store.Fleet.Redis: [returned] :: PoolChange -> ![PoolTx]
+ Haskoin.Store.Fleet.Redis: [signals] :: PoolTx -> !Bool
+ Haskoin.Store.Fleet.Redis: [size] :: FleetKeys -> !Int
+ Haskoin.Store.Fleet.Redis: [spent] :: PoolChange -> ![ByteString]
+ Haskoin.Store.Fleet.Redis: [time] :: PoolTx -> !Word64
+ Haskoin.Store.Fleet.Redis: [tip] :: FleetKeys -> !ByteString
+ Haskoin.Store.Fleet.Redis: [txid] :: PoolTx -> !ByteString
+ Haskoin.Store.Fleet.Redis: data FleetEvent
+ Haskoin.Store.Fleet.Redis: data FleetKeys
+ Haskoin.Store.Fleet.Redis: data Moved
+ Haskoin.Store.Fleet.Redis: data Offered
+ Haskoin.Store.Fleet.Redis: data PoolChange
+ Haskoin.Store.Fleet.Redis: data PoolEntry
+ Haskoin.Store.Fleet.Redis: data PoolTx
+ Haskoin.Store.Fleet.Redis: fleetAdvance :: Connection -> FleetKeys -> Int -> Maybe Time -> Maybe ByteString -> ByteString -> Maybe PoolChange -> [ByteString] -> IO Moved
+ Haskoin.Store.Fleet.Redis: fleetEventsAfter :: Connection -> FleetKeys -> ByteString -> Integer -> IO [(ByteString, FleetEvent)]
+ Haskoin.Store.Fleet.Redis: fleetEventsEnd :: Connection -> FleetKeys -> IO ByteString
+ Haskoin.Store.Fleet.Redis: fleetKeys :: ByteString -> Int -> FleetKeys
+ Haskoin.Store.Fleet.Redis: fleetLive :: Connection -> FleetKeys -> IO [(ByteString, Time)]
+ Haskoin.Store.Fleet.Redis: fleetMove :: Connection -> FleetKeys -> Maybe Time -> Maybe ByteString -> IO Moved
+ Haskoin.Store.Fleet.Redis: fleetReport :: Connection -> FleetKeys -> Maybe Time -> Time -> ByteString -> [ByteString] -> [ByteString] -> IO Bool
+ Haskoin.Store.Fleet.Redis: fleetTip :: Connection -> FleetKeys -> IO (Maybe ByteString)
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.FleetEvent
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.Moved
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.Offered
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.PoolChange
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.PoolEntry
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Eq Haskoin.Store.Fleet.Redis.PoolTx
+ Haskoin.Store.Fleet.Redis: instance GHC.Classes.Ord Haskoin.Store.Fleet.Redis.FleetEvent
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.FleetEvent
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.Moved
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.Offered
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.PoolChange
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.PoolEntry
+ Haskoin.Store.Fleet.Redis: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.Redis.PoolTx
+ Haskoin.Store.Fleet.Redis: poolBody :: Connection -> FleetKeys -> ByteString -> IO (Maybe ByteString)
+ Haskoin.Store.Fleet.Redis: poolOffer :: Connection -> FleetKeys -> Int -> Maybe ByteString -> PoolTx -> [Bool] -> [ByteString] -> IO Offered
+ Haskoin.Store.Fleet.Redis: poolRead :: Connection -> FleetKeys -> Int -> Maybe ByteString -> ByteString -> [ByteString] -> IO (Maybe [ByteString])
+ Haskoin.Store.Fleet.Redis: poolSpends :: Connection -> FleetKeys -> IO [(ByteString, ByteString)]
+ Haskoin.Store.Fleet.Redis: poolWithdraw :: Connection -> FleetKeys -> Int -> [ByteString] -> IO [ByteString]
+ Haskoin.Store.Fleet.XPub: Abandoned :: Built
+ Haskoin.Store.Fleet.XPub: AddressChange :: ![(TxHash, BlockRef)] -> ![(OutPoint, BlockRef, Word64, ByteString)] -> ![OutPoint] -> AddressChange
+ Haskoin.Store.Fleet.XPub: Built :: !Int -> Built
+ Haskoin.Store.Fleet.XPub: CacheEmptied :: CacheMoved
+ Haskoin.Store.Fleet.XPub: CacheMoved :: CacheMoved
+ Haskoin.Store.Fleet.XPub: CacheRaced :: CacheMoved
+ Haskoin.Store.Fleet.XPub: CacheStays :: CacheMoved
+ Haskoin.Store.Fleet.XPub: Entry :: ![XPubBal] -> !(Int, Int) -> HashMap Address [Word32] -> Entry
+ Haskoin.Store.Fleet.XPub: Hit :: !a -> Cached a
+ Haskoin.Store.Fleet.XPub: Miss :: Cached a
+ Haskoin.Store.Fleet.XPub: Moved :: Cached a
+ Haskoin.Store.Fleet.XPub: [balances] :: Entry -> ![XPubBal]
+ Haskoin.Store.Fleet.XPub: [paid] :: AddressChange -> ![(OutPoint, BlockRef, Word64, ByteString)]
+ Haskoin.Store.Fleet.XPub: [paths] :: Entry -> HashMap Address [Word32]
+ Haskoin.Store.Fleet.XPub: [sizes] :: Entry -> !(Int, Int)
+ Haskoin.Store.Fleet.XPub: [spent] :: AddressChange -> ![OutPoint]
+ Haskoin.Store.Fleet.XPub: [txs] :: AddressChange -> ![(TxHash, BlockRef)]
+ Haskoin.Store.Fleet.XPub: blockChanges :: Network -> Ctx -> [TxData] -> HashMap Address AddressChange
+ Haskoin.Store.Fleet.XPub: cacheAdvance :: Connection -> FleetKeys -> Maybe ByteString -> ByteString -> Maybe [(Address, Maybe Balance, AddressChange)] -> IO CacheMoved
+ Haskoin.Store.Fleet.XPub: cacheBuild :: Connection -> FleetKeys -> ByteString -> DatabaseReader -> PoolReads -> Word64 -> XPubSpec -> IO Built
+ Haskoin.Store.Fleet.XPub: cacheDrop :: Connection -> FleetKeys -> XPubSpec -> IO ()
+ Haskoin.Store.Fleet.XPub: cacheEvict :: Connection -> FleetKeys -> Int -> IO Int
+ Haskoin.Store.Fleet.XPub: cacheHave :: Connection -> FleetKeys -> XPubSpec -> IO (Int, Int)
+ Haskoin.Store.Fleet.XPub: cacheSize :: Connection -> FleetKeys -> IO (Int, Int)
+ Haskoin.Store.Fleet.XPub: cacheTip :: Connection -> FleetKeys -> IO (Maybe ByteString)
+ Haskoin.Store.Fleet.XPub: cacheTracked :: Connection -> FleetKeys -> [Address] -> IO [Bool]
+ Haskoin.Store.Fleet.XPub: cachedCount :: Connection -> FleetKeys -> ByteString -> XPubSpec -> (Int, Int) -> IO (Cached Word32)
+ Haskoin.Store.Fleet.XPub: cachedEntry :: Connection -> FleetKeys -> ByteString -> Word32 -> Word32 -> Word64 -> XPubSpec -> IO (Cached Entry)
+ Haskoin.Store.Fleet.XPub: cachedTxs :: Connection -> FleetKeys -> ByteString -> XPubSpec -> (Int, Int) -> From -> Limits -> IO (Cached [TxRef])
+ Haskoin.Store.Fleet.XPub: cachedUnspents :: Connection -> FleetKeys -> ByteString -> XPubSpec -> Entry -> From -> Limits -> IO (Cached [XPubUnspent])
+ Haskoin.Store.Fleet.XPub: data AddressChange
+ Haskoin.Store.Fleet.XPub: data Built
+ Haskoin.Store.Fleet.XPub: data CacheMoved
+ Haskoin.Store.Fleet.XPub: data Cached a
+ Haskoin.Store.Fleet.XPub: data Entry
+ Haskoin.Store.Fleet.XPub: instance GHC.Classes.Eq Haskoin.Store.Fleet.XPub.AddressChange
+ Haskoin.Store.Fleet.XPub: instance GHC.Classes.Eq Haskoin.Store.Fleet.XPub.Built
+ Haskoin.Store.Fleet.XPub: instance GHC.Classes.Eq Haskoin.Store.Fleet.XPub.CacheMoved
+ Haskoin.Store.Fleet.XPub: instance GHC.Classes.Eq Haskoin.Store.Fleet.XPub.Entry
+ Haskoin.Store.Fleet.XPub: instance GHC.Classes.Eq a => GHC.Classes.Eq (Haskoin.Store.Fleet.XPub.Cached a)
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Base.Functor Haskoin.Store.Fleet.XPub.Cached
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Base.Semigroup Haskoin.Store.Fleet.XPub.AddressChange
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.XPub.AddressChange
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.XPub.Built
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.XPub.CacheMoved
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Show.Show Haskoin.Store.Fleet.XPub.Entry
+ Haskoin.Store.Fleet.XPub: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Haskoin.Store.Fleet.XPub.Cached a)
+ Haskoin.Store.Fleet.XPub: nothingPool :: PoolReads
+ Haskoin.Store.Fleet.XPub: position :: Int64 -> BlockRef
+ Haskoin.Store.Fleet.XPub: score :: BlockRef -> Int64
+ Haskoin.Store.Fleet.XPub: window :: Word32 -> Word32 -> [(Word32, Word32, Balance)] -> Maybe [XPubBal]
+ Haskoin.Store.Fleet.XPub: xpubId :: XPubSpec -> ByteString
+ Haskoin.Store.Logic.Layered: deleteUnconfirmedTx :: Writer -> Bool -> TxHash -> IO ()
+ Haskoin.Store.Logic.Layered: importBlock :: Writer -> Block -> BlockNode -> IO (BlockData, [TxData])
+ Haskoin.Store.Logic.Layered: initBest :: Writer -> IO ()
+ Haskoin.Store.Logic.Layered: newMempoolTx :: Writer -> Tx -> UnixTime -> IO Bool
+ Haskoin.Store.Logic.Layered: revertBlock :: Writer -> BlockHash -> IO ()
+ Haskoin.Store.Manager: [events] :: Store -> !Publisher StoreEvent
+ Haskoin.Store.Manager: [fleet] :: Store -> !Maybe Member
+ Haskoin.Store.Manager: [redisMaxXPubs] :: StoreConfig -> !Int
+ Haskoin.Store.Manager: [redisMinTxs] :: StoreConfig -> !Int
+ Haskoin.Store.Web: BehindFleet :: Except
+ Haskoin.Store.Web: [tls] :: WebConfig -> !Maybe (FilePath, FilePath)
+ Haskoin.Store.Web: acceptsBinary :: Text -> Bool
+ Haskoin.Store.Web: runWebSocket :: WebConfig -> Socket -> IO ()
+ Haskoin.Store.Web.Docs: swaggerPage :: ByteString
+ Haskoin.Store.Web.Docs: swaggerYaml :: ByteString
+ Haskoin.Store.Web.Events: BlockArrived :: !BlockHash -> Happened
+ Haskoin.Store.Web.Events: Entry :: !Word64 -> !Event -> !Maybe Seen -> !Int -> Entry
+ Haskoin.Store.Web.Events: Seen :: !Tx -> !HashSet Address -> !Int -> Seen
+ Haskoin.Store.Web.Events: TxEntered :: !TxHash -> !Maybe Seen -> Happened
+ Haskoin.Store.Web.Events: TxLeft :: !TxHash -> Happened
+ Haskoin.Store.Web.Events: Watch :: !Bool -> !HashSet Address -> !HashMap XPubSpec (Word32, Word32) -> !HashMap Address (XPubSpec, Word32, Word32) -> !Maybe BloomFilter -> Watch
+ Haskoin.Store.Web.Events: [addresses] :: Seen -> !HashSet Address
+ Haskoin.Store.Web.Events: [bloom] :: Watch -> !Maybe BloomFilter
+ Haskoin.Store.Web.Events: [derived] :: Watch -> !HashMap Address (XPubSpec, Word32, Word32)
+ Haskoin.Store.Web.Events: [event] :: Entry -> !Event
+ Haskoin.Store.Web.Events: [filtered] :: Watch -> !Bool
+ Haskoin.Store.Web.Events: [keys] :: Watch -> !HashMap XPubSpec (Word32, Word32)
+ Haskoin.Store.Web.Events: [number] :: Entry -> !Word64
+ Haskoin.Store.Web.Events: [plain] :: Watch -> !HashSet Address
+ Haskoin.Store.Web.Events: [seen] :: Entry -> !Maybe Seen
+ Haskoin.Store.Web.Events: [size] :: Seen -> !Int
+ Haskoin.Store.Web.Events: [tx] :: Seen -> !Tx
+ Haskoin.Store.Web.Events: [weight] :: Entry -> !Int
+ Haskoin.Store.Web.Events: appendEvent :: EventLog -> Happened -> STM ()
+ Haskoin.Store.Web.Events: command :: Ctx -> Word32 -> (XPubSpec -> IO [XPubBal]) -> TVar Watch -> EventCommand -> IO (Either String ())
+ Haskoin.Store.Web.Events: data Entry
+ Haskoin.Store.Web.Events: data EventLog
+ Haskoin.Store.Web.Events: data Happened
+ Haskoin.Store.Web.Events: data Seen
+ Haskoin.Store.Web.Events: data Watch
+ Haskoin.Store.Web.Events: eventsAfter :: EventLog -> Maybe BlockHash -> Maybe TxHash -> STM (Maybe Word64)
+ Haskoin.Store.Web.Events: eventsEnd :: EventLog -> STM Word64
+ Haskoin.Store.Web.Events: everything :: Watch
+ Haskoin.Store.Web.Events: instance GHC.Classes.Eq Haskoin.Store.Web.Events.Entry
+ Haskoin.Store.Web.Events: instance GHC.Classes.Eq Haskoin.Store.Web.Events.Happened
+ Haskoin.Store.Web.Events: instance GHC.Classes.Eq Haskoin.Store.Web.Events.Seen
+ Haskoin.Store.Web.Events: instance GHC.Classes.Eq Haskoin.Store.Web.Events.Watch
+ Haskoin.Store.Web.Events: instance GHC.Internal.Show.Show Haskoin.Store.Web.Events.Entry
+ Haskoin.Store.Web.Events: instance GHC.Internal.Show.Show Haskoin.Store.Web.Events.Happened
+ Haskoin.Store.Web.Events: instance GHC.Internal.Show.Show Haskoin.Store.Web.Events.Seen
+ Haskoin.Store.Web.Events: instance GHC.Internal.Show.Show Haskoin.Store.Web.Events.Watch
+ Haskoin.Store.Web.Events: logLength :: Int
+ Haskoin.Store.Web.Events: logWeight :: Int
+ Haskoin.Store.Web.Events: maxWatchAddresses :: Int
+ Haskoin.Store.Web.Events: maxWatchXPubs :: Int
+ Haskoin.Store.Web.Events: newEventLog :: IO EventLog
+ Haskoin.Store.Web.Events: nothing :: Watch
+ Haskoin.Store.Web.Events: readEvents :: EventLog -> Word64 -> STM (Maybe (Seq Entry))
+ Haskoin.Store.Web.Events: relevant :: Ctx -> Word32 -> Maybe Seen -> Watch -> (Bool, Watch)
+ Haskoin.Store.Web.Events: seenOf :: Network -> Ctx -> TxData -> Seen
+ Haskoin.Store.Web.Router: Next :: Next
+ Haskoin.Store.Web.Router: NoRoute :: Miss
+ Haskoin.Store.Web.Router: Req :: !Request -> ![(Text, Text)] -> ![(Text, Text)] -> !ByteString -> ![Text] -> Req
+ Haskoin.Store.Web.Router: Route :: !Method -> ![Either Text Text] -> ![Text] -> (Req -> IO Response) -> Route
+ Haskoin.Store.Web.Router: Undeclared :: Text -> Undeclared
+ Haskoin.Store.Web.Router: Unknown :: ![Text] -> Miss
+ Haskoin.Store.Web.Router: [accepted] :: Req -> ![Text]
+ Haskoin.Store.Web.Router: [accepts] :: Route -> ![Text]
+ Haskoin.Store.Web.Router: [body] :: Req -> !ByteString
+ Haskoin.Store.Web.Router: [captures] :: Req -> ![(Text, Text)]
+ Haskoin.Store.Web.Router: [handler] :: Route -> Req -> IO Response
+ Haskoin.Store.Web.Router: [method] :: Route -> !Method
+ Haskoin.Store.Web.Router: [params] :: Req -> ![(Text, Text)]
+ Haskoin.Store.Web.Router: [pattern] :: Route -> ![Either Text Text]
+ Haskoin.Store.Web.Router: [wai] :: Req -> !Request
+ Haskoin.Store.Web.Router: capture :: Req -> Text -> IO Text
+ Haskoin.Store.Web.Router: captureWith :: Req -> (Text -> Maybe a) -> Text -> IO a
+ Haskoin.Store.Web.Router: data Miss
+ Haskoin.Store.Web.Router: data Next
+ Haskoin.Store.Web.Router: data Req
+ Haskoin.Store.Web.Router: data Route
+ Haskoin.Store.Web.Router: get :: Text -> [Text] -> (Req -> IO Response) -> Route
+ Haskoin.Store.Web.Router: header :: Req -> Text -> Maybe Text
+ Haskoin.Store.Web.Router: instance GHC.Internal.Exception.Type.Exception Haskoin.Store.Web.Router.Next
+ Haskoin.Store.Web.Router: instance GHC.Internal.Exception.Type.Exception Haskoin.Store.Web.Router.Undeclared
+ Haskoin.Store.Web.Router: instance GHC.Internal.Show.Show Haskoin.Store.Web.Router.Miss
+ Haskoin.Store.Web.Router: instance GHC.Internal.Show.Show Haskoin.Store.Web.Router.Next
+ Haskoin.Store.Web.Router: instance GHC.Internal.Show.Show Haskoin.Store.Web.Router.Undeclared
+ Haskoin.Store.Web.Router: newtype Undeclared
+ Haskoin.Store.Web.Router: next :: IO a
+ Haskoin.Store.Web.Router: param :: Req -> Text -> IO (Maybe Text)
+ Haskoin.Store.Web.Router: post :: Text -> [Text] -> (Req -> IO Response) -> Route
+ Haskoin.Store.Web.Router: route :: Method -> Text -> [Text] -> (Req -> IO Response) -> Route
+ Haskoin.Store.Web.Router: router :: [Route] -> (Req -> Miss -> IO Response) -> Application
+ Haskoin.Store.Web.Router: routerScoped :: [Route] -> (([Route] -> IO (Maybe ResponseReceived)) -> IO (Maybe ResponseReceived)) -> (Req -> Miss -> IO Response) -> Application
- Haskoin.Store: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Maybe CacheMetrics -> !Logger -> CacheConfig
+ Haskoin.Store: CacheConfig :: !String -> !Int -> !Int -> !Int -> !Maybe Stats -> CacheConfig
- Haskoin.Store: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
+ Haskoin.Store: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe Cache -> !Maybe Member -> !Publisher StoreEvent -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
- Haskoin.Store: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Int -> !Bool -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Logger -> StoreConfig
+ Haskoin.Store: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Bool -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Logger -> StoreConfig
- Haskoin.Store: [block] :: Unspent -> !BlockRef
+ Haskoin.Store: [block] :: Store -> !BlockStore
- Haskoin.Store: [cache] :: Store -> !Maybe CacheConfig
+ Haskoin.Store: [cache] :: Store -> !Maybe Cache
- Haskoin.Store: [chain] :: CacheConfig -> !Chain
+ Haskoin.Store: [chain] :: Store -> !Chain
- Haskoin.Store: [ctx] :: StoreConfig -> !Ctx
+ Haskoin.Store: [ctx] :: StoreRead -> !Ctx
- Haskoin.Store: [logger] :: CacheConfig -> !Logger
+ Haskoin.Store: [logger] :: StoreConfig -> !Logger
- Haskoin.Store: [net] :: StoreConfig -> !Network
+ Haskoin.Store: [net] :: StoreRead -> !Network
- Haskoin.Store: [redis] :: CacheConfig -> !Connection
+ Haskoin.Store: [redis] :: StoreConfig -> !Maybe String
- Haskoin.Store: [stats] :: StoreConfig -> !Maybe Stats
+ Haskoin.Store: [stats] :: CacheConfig -> !Maybe Stats
- Haskoin.Store: blockAtOrBefore :: (MonadIO m, StoreReadExtra m) => Chain -> UnixTime -> m (Maybe BlockData)
+ Haskoin.Store: blockAtOrBefore :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)
- Haskoin.Store: getActiveTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)
+ Haskoin.Store: getActiveTxData :: StoreRead -> TxHash -> IO (Maybe TxData)
- Haskoin.Store: getDefaultBalance :: StoreReadBase m => Address -> m Balance
+ Haskoin.Store: getDefaultBalance :: StoreRead -> Address -> IO Balance
- Haskoin.Store: getTransaction :: StoreReadBase m => TxHash -> m (Maybe Transaction)
+ Haskoin.Store: getTransaction :: StoreRead -> TxHash -> IO (Maybe Transaction)
- Haskoin.Store: getTransactions :: StoreReadBase m => [TxHash] -> m [Maybe Transaction]
+ Haskoin.Store: getTransactions :: StoreRead -> [TxHash] -> IO [Maybe Transaction]
- Haskoin.Store: withStore :: MonadUnliftIO m => StoreConfig -> (Store -> m a) -> m a
+ Haskoin.Store: withStore :: StoreConfig -> (Store -> IO a) -> IO a
- Haskoin.Store.BlockStore: BlockStoreConfig :: !Ctx -> !PeerMgr -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !Maybe Stats -> !Logger -> BlockStoreConfig
+ Haskoin.Store.BlockStore: BlockStoreConfig :: !Ctx -> !PeerMgr -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !BlockHash -> IO () -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !Maybe Stats -> !Logger -> BlockStoreConfig
- Haskoin.Store.BlockStore: blockStoreBlock :: MonadIO m => Peer -> Block -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStoreBlock :: Peer -> Block -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStoreHead :: MonadIO m => BlockNode -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStoreHead :: BlockNode -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStoreNotFound :: MonadIO m => Peer -> [BlockHash] -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStoreNotFound :: Peer -> [BlockHash] -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStorePeerConnect :: MonadIO m => Peer -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStorePeerConnect :: Peer -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStorePeerDisconnect :: MonadIO m => Peer -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStorePeerDisconnect :: Peer -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStorePendingTxs :: MonadIO m => BlockStore -> m Int
+ Haskoin.Store.BlockStore: blockStorePendingTxs :: BlockStore -> IO Int
- Haskoin.Store.BlockStore: blockStoreTx :: MonadIO m => Peer -> Tx -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStoreTx :: Peer -> Tx -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: blockStoreTxHash :: MonadIO m => Peer -> [TxHash] -> BlockStore -> m ()
+ Haskoin.Store.BlockStore: blockStoreTxHash :: Peer -> [TxHash] -> BlockStore -> IO ()
- Haskoin.Store.BlockStore: withBlockStore :: MonadUnliftIO m => BlockStoreConfig -> (BlockStore -> m a) -> m a
+ Haskoin.Store.BlockStore: withBlockStore :: BlockStoreConfig -> (BlockStore -> IO a) -> IO a
- Haskoin.Store.Cache: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Maybe CacheMetrics -> !Logger -> CacheConfig
+ Haskoin.Store.Cache: CacheConfig :: !String -> !Int -> !Int -> !Int -> !Maybe Stats -> CacheConfig
- Haskoin.Store.Cache: withCache :: Maybe CacheConfig -> CacheT m a -> m a
+ Haskoin.Store.Cache: withCache :: CacheConfig -> DatabaseReader -> Chain -> Publisher StoreEvent -> Logger -> (Cache -> IO a) -> IO a
- Haskoin.Store.Common: blockAtOrAfter :: (MonadIO m, StoreReadExtra m) => Chain -> UnixTime -> m (Maybe BlockData)
+ Haskoin.Store.Common: blockAtOrAfter :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)
- Haskoin.Store.Common: blockAtOrAfterMTP :: (MonadIO m, StoreReadExtra m) => Chain -> UnixTime -> m (Maybe BlockData)
+ Haskoin.Store.Common: blockAtOrAfterMTP :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)
- Haskoin.Store.Common: blockAtOrBefore :: (MonadIO m, StoreReadExtra m) => Chain -> UnixTime -> m (Maybe BlockData)
+ Haskoin.Store.Common: blockAtOrBefore :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)
- Haskoin.Store.Common: getActiveBlock :: StoreReadExtra m => BlockHash -> m (Maybe BlockData)
+ Haskoin.Store.Common: getActiveBlock :: StoreRead -> BlockHash -> IO (Maybe BlockData)
- Haskoin.Store.Common: getActiveTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)
+ Haskoin.Store.Common: getActiveTxData :: StoreRead -> TxHash -> IO (Maybe TxData)
- Haskoin.Store.Common: getDefaultBalance :: StoreReadBase m => Address -> m Balance
+ Haskoin.Store.Common: getDefaultBalance :: StoreRead -> Address -> IO Balance
- Haskoin.Store.Common: getNumTransaction :: StoreReadExtra m => Word64 -> m [Transaction]
+ Haskoin.Store.Common: getNumTransaction :: StoreRead -> Word64 -> IO [Transaction]
- Haskoin.Store.Common: getTransaction :: StoreReadBase m => TxHash -> m (Maybe Transaction)
+ Haskoin.Store.Common: getTransaction :: StoreRead -> TxHash -> IO (Maybe Transaction)
- Haskoin.Store.Common: getTransactions :: StoreReadBase m => [TxHash] -> m [Maybe Transaction]
+ Haskoin.Store.Common: getTransactions :: StoreRead -> [TxHash] -> IO [Maybe Transaction]
- Haskoin.Store.Common: joinDescStreams :: forall (m :: Type -> Type) a. (Monad m, Ord a) => [ConduitT () a m ()] -> ConduitT () a m ()
+ Haskoin.Store.Common: joinDescStreams :: forall (m :: Type -> Type) k a. (Monad m, Ord k) => (a -> k) -> [ConduitT () a m ()] -> ConduitT () a m ()
- Haskoin.Store.Common: mergeDescGroups :: forall (m :: Type -> Type) a k. (Monad m, Ord a, Ord k) => (a -> k) -> Maybe Word32 -> [ConduitT () a m ()] -> ConduitT () a m ()
+ Haskoin.Store.Common: mergeDescGroups :: forall (m :: Type -> Type) o k a. (Monad m, Ord o, Ord k) => (a -> k) -> (a -> o) -> Maybe Word32 -> [ConduitT () a m ()] -> ConduitT () a m ()
- Haskoin.Store.Common: microseconds :: MonadIO m => m Integer
+ Haskoin.Store.Common: microseconds :: IO Integer
- Haskoin.Store.Database.Reader: withDatabaseReader :: MonadUnliftIO m => Network -> Ctx -> Word32 -> Word32 -> FilePath -> Bool -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe DataMetrics -> DatabaseReaderT m a -> m a
+ Haskoin.Store.Database.Reader: withDatabaseReader :: Network -> Ctx -> Word32 -> Word32 -> FilePath -> Bool -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe DataMetrics -> (DatabaseReader -> IO a) -> IO a
- Haskoin.Store.Database.Writer: runWriter :: MonadIO m => Network -> Ctx -> DatabaseReader -> Logger -> WriterT m a -> m a
+ Haskoin.Store.Database.Writer: runWriter :: Network -> Ctx -> DatabaseReader -> Logger -> (Writer -> IO a) -> IO a
- Haskoin.Store.Database.Writer: runWriterMempool :: MonadIO m => Network -> Ctx -> DatabaseReader -> Logger -> WriterT m a -> m (a, MempoolChanges)
+ Haskoin.Store.Database.Writer: runWriterMempool :: Network -> Ctx -> DatabaseReader -> Logger -> (Writer -> IO a) -> IO (a, MempoolChanges)
- Haskoin.Store.Database.Writer: writerLogger :: forall (m :: Type -> Type). Monad m => WriterT m Logger
+ Haskoin.Store.Database.Writer: writerLogger :: Writer -> Logger
- Haskoin.Store.Logic: deleteUnconfirmedTx :: Bool -> TxHash -> ImportM ()
+ Haskoin.Store.Logic: deleteUnconfirmedTx :: Writer -> Bool -> TxHash -> IO ()
- Haskoin.Store.Logic: importBlock :: Block -> BlockNode -> ImportM (BlockData, [TxData])
+ Haskoin.Store.Logic: importBlock :: Writer -> Block -> BlockNode -> IO (BlockData, [TxData])
- Haskoin.Store.Logic: initBest :: ImportM ()
+ Haskoin.Store.Logic: initBest :: Writer -> IO ()
- Haskoin.Store.Logic: newMempoolTx :: Tx -> UnixTime -> ImportM Bool
+ Haskoin.Store.Logic: newMempoolTx :: Writer -> Tx -> UnixTime -> IO Bool
- Haskoin.Store.Logic: revertBlock :: BlockHash -> ImportM ()
+ Haskoin.Store.Logic: revertBlock :: Writer -> BlockHash -> IO ()
- Haskoin.Store.Manager: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
+ Haskoin.Store.Manager: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe Cache -> !Maybe Member -> !Publisher StoreEvent -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
- Haskoin.Store.Manager: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Int -> !Bool -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Logger -> StoreConfig
+ Haskoin.Store.Manager: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> !Bool -> !Int -> !Int -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Bool -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Maybe Int -> !Logger -> StoreConfig
- Haskoin.Store.Manager: [cache] :: Store -> !Maybe CacheConfig
+ Haskoin.Store.Manager: [cache] :: Store -> !Maybe Cache
- Haskoin.Store.Manager: withStore :: MonadUnliftIO m => StoreConfig -> (Store -> m a) -> m a
+ Haskoin.Store.Manager: withStore :: StoreConfig -> (Store -> IO a) -> IO a
- Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !Int -> !WebLimits -> !String -> !Bool -> !Maybe Stats -> !Int -> !String -> !String -> !Bool -> !Bool -> !Int -> !Logger -> WebConfig
+ Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !Int -> !WebLimits -> !String -> !Bool -> !Maybe Stats -> !Int -> !String -> !String -> !Bool -> !Bool -> !Int -> !Maybe (FilePath, FilePath) -> !Logger -> WebConfig
- Haskoin.Store.Web: runWeb :: MonadUnliftIO m => WebConfig -> m ()
+ Haskoin.Store.Web: runWeb :: WebConfig -> IO ()

Files

+ CHANGELOG.md view
@@ -0,0 +1,2828 @@+# Changelog+All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html)++## [Unreleased]++## [1.9.0] - 2026-11-10++### Added++- `--fleet` (`FLEET`): servers that share a Redis answer as one. Every+  request is answered from the newest block that all live servers have and+  from one mempool that they share in Redis, so that the same request gets+  the same answer whichever server a load balancer picks, also while a+  block or a transaction is on its way through them.+  - Each server keeps its database as it was at each of its last six+    blocks, reports them under a lease of ten seconds, and offers its+    unconfirmed transactions to the shared pool. The fleet tip moves in one+    step, together with what the blocks in between did to the pool.+  - A server that does not have the block that the fleet answers from says+    503 with `behind-fleet`. One whose health check fails stops reporting,+    and stops counting when its lease runs out.+  - Without Redis a server answers from its own database and `/health` says+    203.+  - `Haskoin.Store.Manager` runs the member (`Store.fleet`), the block+    store takes a view of the database right after each block+    (`BlockStoreConfig.onBest`), and the web server answers through+    `Haskoin.Store.Database.Query.Pooled`.+  - An integration test runs two servers as a fleet against a real node and+    Redis and compares their answers with a payment in the pool and after+    the block that confirms it.+  - `docs/layered-database/FleetCost.hs` times a store alone against the+    same store in a fleet, with 40,000 transactions in the pool: a fleet+    adds about one round trip to Redis to a request. A request is answered+    from the tip that the member last read, which its first read of the+    pool checks; a transaction's body comes with what spends its outputs;+    and a first page is as large as the page asked for.++- A cache of xpubs for a fleet (`Haskoin.Store.Fleet.XPub`), under the+  options of the cache of a server alone: `--redis-min-addrs`,+  `--redis-min-txs` and `--redis-max-xpubs`. Until now `--fleet` answered+  every request about an xpub from the database, with a read of the shared+  pool for each address that it touches.+  - An entry has the chain's part of a wallet at one block, which is the+    same for the whole cache, and the part of the shared pool, which the+    script of the pool keeps as it changes the pool. Transactions and+    outputs of both are in one sorted set each, in the order of the API.+  - The cache answers only while its block is the fleet tip. The server+    that moves the tip brings the cache along, with what the new blocks+    did at the addresses that entries have. If it does not, or after a+    reorganisation, the cache is emptied.+  - The cache is at a move of the fleet tip, which the pool counts, and+    not just at a block: the tip can leave a block and come back to it.+  - Every read of balances runs the scan of the xpub over the entry and+    misses unless it reads exactly the addresses of the entry, so a+    payment in the pool to an unused address cannot leave the gap wrong.+  - Entries are built in the background by the server that missed, and+    extended when an xpub gains addresses.+  - A request about a wallet of 464 addresses and 19,499 transactions+    takes 1.3 to 2.2 milliseconds through it, where a fleet took 5 to 38;+    for a wallet with 400 addresses in the pool, 3 to 4 where it took 5 to+    126 (`docs/layered-database/fleet-cost.txt`).+  - The test of the member compares every answer about a wallet through+    the cache with the uncached one after each step of random histories,+    and the two servers of the integration test do the same over HTTP.+  - StatsD metrics under `fleet.cache.*`: hits, misses, builds and their+    time, builds given up, evictions, and the times that the cache was+    emptied. The log says why the cache was emptied each time, and with+    `--debug` has a line for each entry built and each block that the+    cache was brought to.+- Built with `rocksdb-haskell-jprupp` 2.7.1, and every scan by address+  uses an iterator that RocksDB stops where the address ends, so that a+  scan cannot walk over deleted keys that follow it. The migration reads+  the transactions with an iterator that is asked for the order of them+  all.+- Built with `haskoin-node` 2.5.0, which has the fixes of its security+  review: among them, a block from a peer is checked against its header+  before the store sees it, so a peer can no longer pass off transactions+  of its choice under a real header. It also allows the new binding. On+  regtest its discovery no longer finds a node on this machine: name the+  node with `--peer`.+- **`/events` agrees across a fleet.** The servers of a fleet send the+  events of the block and the mempool that they share, read from a stream+  in Redis that the pool's script writes as it makes each change, and no+  longer those of their own nodes. A block is told when the fleet has+  moved to it, with every block that it moved through.+- **`/events` takes up where a client left.** `?after=` with the hash of+  the last event heard starts the stream after it, over HTTP and over the+  WebSocket. The server keeps its last 10,000 events in one log that all+  clients read (`Haskoin.Store.Web.Events`), in place of a queue for each.+  It answers 404 when it does not have the event. The servers of a fleet+  keep the same events, so a client may come back to another one.+- **A WebSocket client chooses the transactions it hears about**: by+  address, by extended public key with a type of address, or with a Bloom+  filter as in BIP 37, with the commands of `EventCommand` in+  `haskoin-store-data` 1.11. Blocks are always sent, and a client that+  sends no command hears everything, as before. A key is watched up to its+  gap, which moves on with its payments.+- **The choice can be in the URL of `/events`**, so that it holds from+  the first event: `quiet=true` for blocks alone, `addresses`, `xpub` with+  `derive` (one type or several), and `filter` with `functions`, `tweak`+  and `flags`. The HTTP stream takes them too, also under `POST` with a+  form. `{"type":"none"}` goes back to blocks alone.+- **The WebSocket speaks binary too**: binary frames when the handshake+  asks for binary or the client sends a binary frame, for events and+  commands alike.+- **HTTPS**, optional and off by default: `--tls-cert` and `--tls-key`+  (`TLS_CERT`, `TLS_KEY`) make the server speak HTTPS on its port.+- **The reference of the API at the root of the server.** `/` is a page+  that shows `swagger.yaml` as it was when the server was built, and can+  try requests against it; `/swagger.yaml` is the file.+- **`HEAD` and `OPTIONS`** are answered: the headers of what `GET` gives,+  and what a browser may send from another site.+- **Answers are compressed** with gzip for clients that accept it, but for+  the stream of events.+- **Lists in a body.** The endpoints that take a list of addresses,+  blocks, heights or transactions accept `POST` with the parameters in a+  form, so that a long list need not fit in a URL.+- **`Retry-After`** on every 503.+- **Errors in binary.** A request that asks for the binary format gets+  its errors in that format too, the 503 of a server behind its fleet+  included. They were JSON whatever was asked. This needs+  `haskoin-store-data` 1.10, whose client reads both.+- **The `Accept` header is read as a list with weights.** Binary is sent+  when `application/octet-stream` is listed with a weight above 0 that is+  no less than that of `application/json`. The header used to be compared+  with that one string, so a list or a weight silently gave JSON. Every+  answer has `Vary: Accept`.+- **The raw lists start with a count**, in binary: `/transactions/raw`,+  `/block/{block}/transactions/raw` and `/block/height/{height}/raw` were+  the one kind of list without it. A binary client of those three+  endpoints has to change with the server.+- `haskoin-store --help` names the environment variable of every option.+- `swagger.yaml` documents the binary format: how to ask for it, the+  encoding of each type that the API returns, and what differs from JSON.+- Gauges `fleet.cache.xpubs` and `fleet.cache.addresses`: how many xpubs+  the cache of a fleet holds, and how many addresses between them.++### Changed++- A WebSocket client of `/events` that sends something the server cannot+  read as a command is disconnected, with code 1008 and the reason. What+  clients sent used to be ignored.+- The HTTP stream of `/events` no longer holds a view of the database for+  as long as its client listens.++- Since version 2.6.4 the binding reads the key and the value of a row of+  an iterator without the cost of a safe foreign call.+  Every list and every count takes between a half and a third of the time:+  the first 100 transactions of a busy address 221 microseconds instead of+  575, and the number of transactions of a wallet with 19,499 of them 34+  milliseconds instead of 78.+- **Bloom filters are on by default**, as was always meant: until the+  binding was fixed, `--bloom` built none. `--no-bloom`, or `BLOOM=false`,+  turns them off; `--bloom` is still accepted. An existing database gets+  filters, and the memory that they take in the block cache, as its table+  files are written again.+- **Transactions that leave the mempool unconfirmed are no longer kept.**+  The API already answered 404 for them, but their bodies stayed in the+  database, marked as deleted, for ever, and each server had its own.+  - A server remembers the last hundred thousand that it deleted, in+    memory, so as not to fetch them again from its peers. The fleet's rule+    for conflicting transactions and the cache of a server alone, which+    both asked the database whether a transaction had been deleted, ask+    that memory instead.+  - The cache of a server alone takes a transaction that Redis has as+    unconfirmed for more than ten minutes, and that the database does not+    have, for deleted and not for one that it has not seen yet.+  - Bodies that version 20 marked as deleted stay in a migrated database+    and are ignored. A page that starts at a deleted transaction is empty.+- Removed reports that no longer describe the store: five intermediate+  stress runs, the measurement of a mempool mirrored in memory, and the+  superseded runs in `docs/layered-database/query-cost.txt`.+  `docs/layered-database.md` is rewritten as a description of what+  exists, a quarter of its length.+- **Database version 22: the record of a confirmed transaction is written+  once**, by the block that confirms it, and is the same on every server+  that has the block, so that what the chain's column families hold+  follows from the chain alone.+  - **Its time is the time of its block.** A server that had seen a+    transaction in its mempool used to keep the time of that when a block+    confirmed it: two servers then gave two values of `time` for the same+    confirmed transaction, which the servers of a fleet must not do. A+    transaction that returns to the mempool when its block leaves the main+    chain has the time of that block there.+  - **The spenders of its outputs are under keys of their own**, in the+    `spender` column family, sixteen outputs to a key (`SpenderKey`).+    They were in the record, which each block that spent one of its+    outputs read and wrote whole again. A spend now reads and writes a+    value of a few dozen bytes and leaves the record alone.+  - Reading a confirmed transaction in full takes a second key: 18+    microseconds where it took 11.5, and 0.8 milliseconds for a hundred+    where it took 0.4. Lists by address and by xpub cost the same, and so+    does writing, on the small records of the measurements.+  - A database of version 21 is migrated the first time the store opens+    it (`migrateRecords`). **It reads every transaction of the database+    and writes nearly every one again**, since nothing lists those that+    need it and most have a spender in them: many hours for a database of+    Bitcoin, and room for a second copy of its transactions until RocksDB+    has compacted the first away. The log says how far it is, and it can+    be interrupted and started again.+  - The tests that run the store through random histories now choose them+    from a database of version 20 that goes through the same+    (`withShadow`). Given a layered database, the chooser found next to+    nothing in the mempool, and the blocks of the tests of the fleet+    confirmed almost none of it.+- **Database version 21: the mempool has an area of its own.** Unconfirmed+  transactions are no longer written into the indices of the chain. They+  are kept apart and applied on top when a query is answered+  (`docs/layered-database.md`), so the chain's data changes only when a+  block is imported or reverted.+  - A database of version 20 is migrated the first time the store opens it,+    and the log says how many mempool transactions were moved. **Older+    stores refuse the database afterwards**: copy a production database+    before upgrading.+  - The block store imports with the rules of `Haskoin.Store.Logic.Layered`.+    A block that spends an output of a transaction that is only in the+    mempool is refused as an orphan; no valid block does that.+  - Each request of the web server reads one snapshot of the database for+    everything it answers, so the parts of a response agree with each+    other. `routerScoped` in `Haskoin.Store.Web.Router` is what gives a+    request its snapshot.+  - `--wipe-mempool` deletes the keys of the mempool area, in less time+    than it took to delete each transaction by the rules, and it no longer+    keeps the wiped transactions as deleted ones.+  - `StoreRead` has a new query, `getAddressesTxCount`.+  - Programs that embed the store and read its database with+    `Haskoin.Store.Database.Query` have to move to+    `Haskoin.Store.Database.Query.Layered`: the old queries do not see the+    mempool of a database of version 21.+- Xpub addresses are derived up to an exact gap: every address up to the last+  used one of each chain, then exactly `--xpub-gap` unused ones, and always+  the first `--xpub-gap-init` addresses. Addresses used to be read 32 at a+  time with the gap checked between batches, so the store looked between 33+  and 64 addresses past the last used one, and started counting again at index+  20. This is why api.haskoin.com saw a payment 34 addresses past the last+  used one, which wallets with a gap of 20 or 32 do not see. Payments that far+  away are no longer seen; raise `--xpub-gap` to see them.+- New Redis cache of xpubs (`Haskoin.Store.Cache`), replacing the old one:+  - Whenever a transaction touches a cached xpub, or a reorganisation changes+    one of its transactions, the store reads the whole xpub again from its+    database and replaces the entry in one Lua script, so cached answers are+    always answers of the database.+  - Everything a response needs is in Redis, transaction bodies included,+    shared between xpubs, with the spenders of their outputs kept up to date.+    A store that has not imported a transaction yet still serves it.+  - Several stores can share one Redis. Each change of a cached xpub is+    claimed and applied once, by the first store to see it; stores that catch+    up find their changes done and do no work. A store never replaces an entry+    that knows more than its database (a higher block, or a transaction it has+    not seen), so stores do not undo each other's work, and every entry is+    written together with the bodies it needs, so a store that is behind can+    serve it in full.+  - The rules for claims and for replacing entries are pure decisions+    (`Haskoin.Store.Cache.Protocol`). A model test runs several stores+    catching up with a chain in random interleavings against a model of+    Redis, and checks that they end with the entry of the tip, that no+    store replaces an entry with one that knows less, that each change is+    written once, and that stores catching up after the first write nothing.+  - A reload writes only the differences between the xpub in the database+    and its entry in Redis, read in chunks first, instead of rewriting the+    whole entry; its script checks that the entry did not change since.+    Rewriting an xpub of 15,000 transactions held Redis for up to 77 ms,+    during which it answered no other client. A store reloads up to four+    xpubs at once.+  - A page of the transactions or unspent outputs of a cached xpub is one+    round trip to Redis: a Lua script finds where it starts, from the+    position of the start transaction in the xpub itself, and reads it.+    It took four or five, and read the whole body of the start+    transaction to find its position.+  - A store marks a cached xpub as used at most every ten seconds, for+    eviction, instead of on every read, which made every read a write.+  - `--redis-min-txs` (`REDIS_MIN_TXS`, default 1000) also caches xpubs+    with at least that many transactions in their addresses, however few+    addresses they have.+  - Keys start with `hs2:<network>:`, so the cache can share a Redis with the+    cache of older versions and with stores of other networks.+  - `--redis-max-xpubs` (`REDIS_MAX_XPUBS`, default 10000) replaces+    `--redis-max-keys`: the least recently used xpubs past this many are+    evicted. `--redis-sync-interval` is removed.+  - StatsD metrics: `cache.hits`, `cache.misses`, `cache.reloads`,+    `cache.stale`, `cache.evictions` and `cache.reload` (milliseconds).+- `n_tx` of `/blockchain/multiaddr` counts every transaction of the shown+  addresses and xpubs, not only those on the page, and `n_tx_filtered` those+  that pass the filter. `n_tx` of `/blockchain/rawaddr` for an xpub is its+  number of transactions, not the size of the page.+- The web server runs directly on WAI and Warp, without Scotty. Routes,+  parameters and passing a request to the next route are in+  `Haskoin.Store.Web.Router`, a small router over WAI; a handler is a plain+  function from the state of the server and the request to a response in+  `IO`, and stops early by throwing an `Except`, which becomes the JSON+  error. Every response carries `Access-Control-Allow-Origin: *`, error+  responses included. The endpoints, their parameters and their responses+  are as before.+- No monad transformers, except conduits, and no functions polymorphic over+  `MonadIO` or `MonadUnliftIO`: everything runs in plain `IO`. Database queries are+  functions that take the `DatabaseReader` (`Haskoin.Store.Database.Query`);+  imports take a `Writer` handle (`Haskoin.Store.Database.Writer`,+  `Haskoin.Store.Logic`); the block store and the manager pass their state+  explicitly. The classes `StoreReadBase` and `StoreReadExtra` are replaced+  by `StoreRead`, a record of the same queries as functions in `IO`, which+  `databaseRead` gives for a database; `getTransaction` and the other+  helpers of `Haskoin.Store.Common` take it as their first argument. The state+  of the web server, which every handler takes as an argument instead of+  reading it from a `ReaderT`, holds the queries to answer with, the cache+  in front of those about xpubs;+  `nocache=true` gives a handler a state whose queries read only the+  database. `withStore`, `runWeb`, `runWebSocket` and the `blockStore*`+  functions are in `IO`. `WriterT`, `ImportM`, the `StoreWrite` class,+  `BlockT`, `CacheT`, `DatabaseReaderT` and `withDB` were removed earlier+  in this release. This changes the library API.+- The package uses the GHC2024 edition, with `DuplicateRecordFields`,+  `NoFieldSelectors`, `OverloadedRecordDot` and `OverloadedStrings` on for+  every module, and explicit deriving strategies.++- haskoin-store-data moves to its own repository,+  [jprupp/haskoin-store-data](https://github.com/jprupp/haskoin-store-data),+  with its history and its own releases, and haskoin-store moves to the root+  of this one; `store/` and `data/` are gone. haskoin-store depends on+  `haskoin-store-data ^>= 1.9`. The package has a full description, PVP+  upper bounds on every dependency, and no `Setup.hs`.+- Legacy addresses are called `legacy` in the `derive` parameter, the default.+  `normal`, the name they had, is still accepted, so old URLs keep working;+  it said nothing about the kind of address. Error messages use the new+  name. Built with haskoin-store-data at commit `b304acd2`.+- The executable gives memory back to the system as soon as its heap no+  longer needs it (`--disable-delayed-os-memory-return` in its default RTS+  options). By default GHC marks such memory free for the kernel to take when+  it runs short, and Linux counts it as resident until then, so the heap of a+  busy store seemed to grow without end, by the size of each peak, with little+  live data. The stress test records the resident GHC heap and how much of it+  is lazily freed. The heap profile and the runs that found this are in+  `stress/results/2026-11-09-memory`, and `scripts/heap-profile-summary.py`+  summarises a heap profile.+- The cache brings an entry up to date from what changed, instead of reading+  the whole xpub from RocksDB and Redis on every change, which the stress test+  showed was almost all the time of a reload, growing to seconds at 100,000+  transactions per xpub. A reload reads the changed transactions, the outputs+  they create and spend at addresses of the xpub, and the balances of its+  addresses, which are compared with Redis's to catch a change the entry+  missed. The whole xpub is still read when it is first cached, when the+  entry is more than a block behind, when an address with transactions enters+  or leaves the gap, when a balance does not match, and every ten minutes.+  Entries keep an index of their unspent outputs by outpoint+  (`x:<id>:uop`), in format version 3; entries of version 2 are upgraded by+  their next reload. New metric `cache.full_reloads`.+- A reload whose balances show changes it was not told of first looks for+  them, instead of reading the whole xpub: the transactions that the database+  has for the addresses whose balances moved, in the mempool or in blocks+  after the one the entry was written at, and the mempool transactions of the+  entry that have since been confirmed or dropped, up to 1,000. Under load the+  database is often ahead of the events being applied, which made about one+  reload in ten of the stress test a full one.+- Bodies that Redis already has, such as those of the transactions that a+  block confirms, are rewritten before the commit of a reload, 200 to a+  script, instead of in it. The commit of a block that confirms a thousand+  transactions of an xpub had about 8,000 arguments and held Redis for 10 to+  30 ms, the only commands of the stress test that took that long.+- The scheduled full reload of a cached xpub comes every five to ten+  minutes, an interval that depends on the xpub, instead of every ten for+  all. Xpubs cached together were read whole together each time: in the full+  stress test, four xpubs of tens of thousands of transactions raised the+  heap from under 100 MiB to 785 MiB for a few seconds every ten minutes.+- `docs/cache-consistency.md` analyses how a cached xpub can differ from the+  database without any check noticing, including spenders of cached+  transactions that no reload repairs, and what would prevent it.+- `docs/layered-database.md`: a design for keeping the mempool out of the+  chain's indices, in an area of its own that is applied on top when a query+  is answered, with the measurements behind it and what it means for the+  cache. The store still runs on the layout of version 20; this adds the+  parts that could be proved ahead of the switch:+  - `Haskoin.Store.Database.Types` has the keys of the mempool area+    (`MemSpenderKey`, `MemAddrKey`, `MemDeletedKey`) and a marker for+    migrations that work in several writes (`MigrationKey`).+  - `Haskoin.Store.Database.Mempool` has the keys that one mempool+    transaction contributes, and what the mempool does to the balance and+    the unspent outputs of an address.+  - `Haskoin.Store.Database.Migrate` moves a database of version 20 to the+    layered layout without a resync: it takes every mempool transaction out+    of the chain's indices and writes it to the mempool area, in atomic+    batches that can be interrupted. Nothing calls it yet.+  - `withLayeredReader` opens a database with the extra column family,+    `mempool-addr`, that the layout needs.+  - Tests build databases with the store's own rules from random histories+    of payments, blocks, replacements, deletions and reorganisations,+    migrate them, and check that every query answers as before, that the+    chain's column families are those of a database that never saw a+    mempool, and that emptying the mempool area leaves the chain's answers.+- `Haskoin.Store.Logic.Layered`: the rules that import and revert blocks+  and add, replace and delete mempool transactions in the layered layout of+  `docs/layered-database.md`, with the functions, results and errors of+  `Haskoin.Store.Logic`. Blocks are written to the chain's column families+  alone, and the mempool to its own area. The store does not use them yet.+  - `Haskoin.Store.Database.Writer` can write the mempool area in the same+    atomic batch as the chain; batches of today's rules write nothing there.+  - Tests give the same random history to both sets of rules, on Bitcoin and+    on Bitcoin Cash: payments, replacements, deletions, blocks in and out of+    order, reorganisations, and transactions and blocks that must be+    refused. Every operation must have the same result and move the same+    transactions in and out of the mempool, every query must answer the same+    after it, and at the end the database of today's rules, once migrated,+    must equal that of the new rules key for key.+  - One difference is on purpose: a block that spends an output of an+    unconfirmed transaction is refused by the new rules, as that output is+    not in the chain. Today it is imported.+- `Haskoin.Store.Database.Query.Layered`: every query of the store for the+  layered layout, with the names and arguments of+  `Haskoin.Store.Database.Query`, and `layeredRead`, which gives them as the+  record of queries that the web server answers with. The store does not use+  them yet.+  - A query takes a `View`, a RocksDB snapshot that only `withView` makes,+    so that the chain and the mempool are read as they were at one moment.+  - Pages are cut from the mempool's part of a list followed by the chain's+    part, by today's rules for starts, offsets and limits, for one address,+    for several, and for extended public keys.+  - Mempool spenders are looked up only for the outputs of a transaction+    that no block spends, in one multi-get for a whole page of transactions.+  - `MemDeletedKeyP` matches deleted transactions by the start of their+    hash, for the short transaction numbers of the Blockchain.info API.+  - Tests ask today's queries and the new ones for pages with random+    limits, offsets and starts, on databases migrated from today's layout,+    on databases built by the rules of the layered layout, and on databases+    upgraded in the middle of a history. They also check that an open view+    goes on answering from its moment while blocks and transactions are+    written.+- A summary for each address that the mempool touches, in the layered+  layout (`MemSummary`, under `MemAddrSumKey`): the number of its mempool+  transactions, and the amounts and numbers of the outputs that the mempool+  pays to it, spends, and both pays and spends. The measurements showed the+  queries of that layout to be too slow for an address with much in the+  mempool, and for any address right after a block, because they scanned+  everything that the mempool has for an address to answer anything.+  - A balance is now two reads by key, and an address with nothing in the+    mempool is found by a key that is not there.+  - Lists of transactions and of unspent outputs read the mempool's entries+    only for addresses that have some, and only as far as the page goes.+  - The rules keep the summary with every transaction that enters or leaves+    the mempool, and the migration writes all of them from the entries that+    it moved. The tests work every summary out again after each operation.+  - Measured again with `QueryCost.hs`: the balance of an address with+    6,216 entries in the mempool takes 11 microseconds instead of 35+    milliseconds, a page of its transactions what it takes in version 20,+    the balances of a wallet of 464 busy addresses 1.9 milliseconds instead+    of 110, and nothing gets slower right after a block. Keeping the+    summaries costs about 110 microseconds for each transaction that passes+    through the mempool. `docs/layered-database.md` has the tables.+- `docs/layered-database/QueryCost.hs` writes one synthetic history into a+  database of each layout and times the rules and the queries of both,+  checking that the answers agree. `docs/layered-database.md` has the+  results and what follows from them: writing costs the same when syncing+  and 14% more for a transaction that passes through the mempool; the new+  queries cost 10 to 15 microseconds more for a request when the mempool+  has nothing for what is asked; and they are too slow, as built, for+  addresses that the mempool touches a lot and for any address right after+  a block. The document proposes a summary for each address in the mempool+  area, read by key, and two changes to the RocksDB binding.+- Documentation: a page that starts at a height has no unconfirmed+  transactions, whatever the height. The comment on `AtBlock` said that a+  height above the best block includes them, and the API reference could be+  read that way. The behaviour is unchanged.+- The stress report breaks the commands that blocked Redis down by script,+  and commits by full and incremental reloads, with their numbers of+  arguments.+- `Haskoin.Store.Fleet`: the rules by which stores that share a Redis agree+  on one block to answer from, the fleet tip, as pure decisions with a model+  test. It is the first part of the design for a fleet that answers as one,+  in `docs/layered-database.md`. Nothing uses it yet.+  `Haskoin.Store.Fleet.Redis` carries the rules out against Redis, with a+  Lua script for each step that has to be one step, and a test that takes+  random sequences of steps through a real Redis and through the rules and+  compares them after each. `Haskoin.Store.Fleet.Pool` has the rules of the+  mempool that such stores share, as of the fleet tip: what may be offered,+  which of two conflicting transactions stays, what leaves and why, and how+  the pool follows the tip forwards and across a reorganisation. A model+  test keeps it valid on top of the tip through random offers, blocks and+  reversals. One more Lua script carries these rules out, moving the tip and+  changing the pool in one step, and is compared with them step by step in a+  real Redis like the others; the comparison found a case that the rules had+  left open, a transaction that comes back from an abandoned block and+  spends what the pool already spends, which now wins. The pool also keeps,+  for each address, what its transactions do there: the transactions and+  the outputs paid to it in the order of a list, the outputs of it that are+  spent, and their sums, so that a balance is one read and a list is read+  by the page, as in a store's own mempool area.+- `Haskoin.Store.Database.Query.Pooled`: the queries of the store with a+  mempool that is not its own on top of the chain, for the shared pool of a+  fleet. `read.lua` and `poolRead` read that pool from Redis, each read+  with a check that the fleet tip has not moved. The tests hold these+  queries to those of the layered layout with the store's own mempool as+  the pool. `fleetAdvance` can empty the pool for a store that is alone and+  does not know the way from the old tip.+- `Haskoin.Store.Fleet.Member`: what a store does as a member of a fleet.+  It keeps a view at each recent block, reports them, moves the fleet tip+  with what the blocks in between did to the pool, offers its mempool, and+  answers from the view at the tip with the pool on top. A test with a real+  Redis takes a store that is alone in its fleet through random histories+  and compares every answer with the store's own after each step. It found+  that a body kept in the pool went stale when a parent was confirmed or+  came back, because a transaction inherits from its unconfirmed parents+  whether it can be replaced by fee; the pool now keeps that apart from the+  body and corrects it as transactions come and go.+- `Haskoin.Store.Database.Views` keeps the database as it was at each recent+  block, a RocksDB snapshot for each, so that a store can answer from the+  block that its fleet agrees on and not only from its newest. Views that+  are no longer needed are let go, and one that a request is reading is+  released when the request is done. Nothing uses it yet.+- A store whose Redis does not answer goes on answering from its database,+  instead of failing every request that the cache would have served. A+  request waits at most two seconds for Redis; after that the store leaves+  Redis alone until a health check finds it again. Meanwhile `/health`+  answers 203, Non-Authoritative Information, with the header+  `Haskoin-Degraded: redis`, when every other check passes: a status in the+  200s, so that load balancers keep the store, and not 200, so that+  monitoring notices. The body of `/health` has a new check, `redis`, for a+  store that has a cache; it does not count towards `ok`. It comes with+  haskoin-store-data 1.9.0, which the store now takes from Hackage.+- Breaking: transactions that were deleted are no longer returned. A+  transaction that was replaced, dropped from the mempool, or was the+  coinbase of a block that left the main chain used to answer by id with+  `"deleted": true`; clients that did not look at that field took it for a+  pending payment. It now gives 404, is left out of lists of transactions+  fetched by id, and a page cannot start at it. The `deleted` field stays in+  responses and is always false. The bodies stay in the database.+- Breaking: parameters are checked. A request with a parameter that its+  endpoint does not accept, such as a misspelt `limt=10` or `pretty` on the+  Blockchain.info endpoints, which never honoured it, gives 400+  `Unknown parameter: …`; and a value that cannot be read, such as+  `notx=yes`, `limit=-1`, `filter=4`, `format=xml` or an unknown currency,+  gives 400 `Invalid value for parameter …`. Both used to be ignored, and+  the client got an answer to a question it did not ask. Requests are+  checked before anything is done. The Blockchain.info endpoints accept+  `format=json`, which their clients often send. Each route of the router+  declares the parameters it accepts, and reading an undeclared one is an+  error of the program, so the two cannot drift apart.+- An xpub asked about with the wrong `derive` gives 400, with a message that+  names the right kind, instead of an empty wallet: when the xpub has no+  transactions with the kind asked for, the store checks the first+  `--xpub-gap-init` addresses of the other kinds. This applies to the `/xpub`+  endpoints and to `/blockchain/rawaddr`. A wallet unused with every kind+  still gets an empty answer, and wallets in use cost nothing more.+- `swagger.yaml` explains the API, not only lists it: an introduction to its+  conventions (amounts, ordering, paging, formats, errors, unconfirmed+  transactions, reorganisations, xpubs, live events, Blockchain.info+  compatibility) and the reasons for them, and a description of every+  endpoint, parameter and field. It documents `/dbstats`, the WebSocket at+  `/events` and the `time` field of the health check, which were missing,+  and corrects the health check examples and the version.+- The README tutorial pages with `height`, the parameter the server reads;+  it said `start`, which the server ignores.+- Every line of code is shorter than 120 columns, `swagger.yaml` included,+  whose long hexadecimal and xpub examples are split into equal pieces that+  parse to the same values. `scripts/format` formats `integration/` and+  `stress/` too.+- Scripts live in files of their own: the Lua scripts of the cache in+  `src/Haskoin/Store/Cache/lua/`, embedded at compile time with+  `file-embed`; the script of wrk in `stress/wrk.lua`, one for every+  endpoint, given its paths on the command line; and the shell steps of CI+  in `scripts/check-local-deps.sh` and `scripts/install-system-libraries.sh`.+  The scripts do the same as before.++### Fixed++- `--bloom` builds Bloom filters. It built none when the store was built+  against RocksDB 6.24 or later, as the Docker image is, because+  `rocksdb-haskell-jprupp` passed the size of the filter to RocksDB with the+  wrong type. Nothing failed; a lookup of a key that is not there just went+  on reading data blocks to find that out. The store is now built with+  `rocksdb-haskell-jprupp` 2.6.3, which has the fix. A store that already+  runs with `--bloom` starts building filters when it is upgraded, for the+  table files written from then on: about 1.25 bytes for each key, held in+  the block cache. Size that on a full database, or turn the option off to+  keep things as they were.+- `--bloom` no longer asks for filters on the two indices by address, which+  are scanned and not read by key. With a filter, a scan of an address that+  has nothing steps over every key deleted since the last flush: 810+  microseconds after 5,000 mempool transactions were deleted, against 13+  without filters. No store was affected, because no filter was ever built;+  with the fixed binding they would have been. The option still builds+  filters for transactions, unspent outputs, blocks, balances and the+  mempool.+- `/blockchain/multiaddr` no longer reads every transaction of the wallet to+  count them. `n_tx` of one xpub is its count (a `ZCARD` when it is cached);+  for several xpubs and addresses, the database counts the distinct+  transactions of all their addresses from the keys of its index. The+  transactions of a page are read in pages of about the size asked for, and+  their bodies a page at a time, instead of one by one; `/blockchain/rawaddr`+  does the same. In the stress test, with xpubs of about 15,000+  transactions, the median time of a multiaddr request went from 3.2 seconds+  to 36 milliseconds, and it no longer grows with the wallet.+- Containers left by an integration test run that was killed are removed+  by the next run, as intended: listing them used a template that Podman+  refuses, so they were never found.+- `--peer HOST[:PORT]` works as documented. It was read as a Haskell+  string, so only `--peer '"HOST:PORT"'`, with the quotes, was accepted.+- The syncing peer is disconnected when it sends a block that was not+  requested, as the documentation of the block store said. The block was+  imported if the node knew its header. The state of the block download is+  now a pure state machine (`Haskoin.Store.BlockStore.Sync`) with property+  tests, which also found that a block listed twice in one request was+  outstanding twice.+- The block store no longer starts a thread for every transaction it+  requests, to forget the request twenty seconds later. Requested and+  received mempool transactions are one pure value+  (`Haskoin.Store.BlockStore.Pending`), with property tests, and old+  requests are forgotten on the clock tick of the block store.+- Build against haskoin-node 2.4.0, from the head of its+  `fix-header-sync-stall` branch. The block store reserves its syncing peer+  as an owner of its own (`reservePeer`, `releasePeer`), so no other part of+  the node can release it; header searches run with `chainRunHeaders`. The+  store stopped following the chain+  when a peer announced new blocks with an inventory message instead of+  headers, which peers do when many blocks are found at once; it stayed at+  the old height until the peer reconnected.+- Mempool transactions that entered the mempool in the same second could be+  listed twice in the transactions of an xpub or a multiaddr wallet, and+  their order differed from page to page. Every merge now orders them as the+  database and the cache store them (`txRefKey`, `unspentKey`).+- `nocache=true` on `/xpub/:xpub/transactions` and `/xpub/:xpub/unspent` no+  longer reads, or fills, the cache.+- `confirmations` of `/blockchain/unspent` no longer wraps around for an+  output in a block newer than the chain of the store.++### Added++- `scripts/stress-cache.sh`, a stress test of the Redis cache: a regtest+  chain, Valkey and wrk in Podman, a haskoin-store server, hundreds of+  thousands of transactions between a few wallets, and wrk on their xpubs+  all the while, with a report (`scripts/stress-report.py`) of reloads,+  lag, resources, the commands that blocked Redis (from its slow log) and+  latencies, and a check of the cached answers at the end. The Podman+  helpers of the integration tests are an internal library (`integration`)+  that the stress program shares, and the test image has wrk in a layer of+  its own.+- `stress/results/2026-11-09/`: the results of a full default stress run,+  kept for comparison with later runs.+- `runWebSocket` serves the API on a socket that already listens. The tests+  bind port 0 and pass the socket, so their web servers get free ports with+  no window in which another program could take them; the containers publish+  their ports on random ports of the host.+- Integration tests against regtest chains of Bitcoin Core and Bitcoin Cash+  Node, with Valkey, in Podman containers built from Ubuntu 26.04 and+  release tarballs checked against pinned hashes, one cached layer per+  download, so nothing is downloaded twice. They are skipped when Podman+  cannot run containers or `HASKOIN_STORE_NO_PODMAN` is set.++## [1.8.1] - 2026-11-07++### Changed++- Documentation: the README explains the purpose and design of the store, with+  installation steps, a full configuration reference, monitoring and a+  tutorial, and the Haddock of every module and function says why it works the+  way it does.+- Build against nqe 0.6.9, rocksdb-haskell-jprupp 2.6.1, rocksdb-query 0.6.1,+  statsd-rupp 0.5.0.2, secp256k1-haskell 2.0.1, haskoin-node 2.3.1 and+  Stackage nightly-2026-11-07.++## [1.8.0] - 2026-11-07++### Changed++- Index the mempool by the time each transaction entered it. `/mempool`,+  `/blockchain/unconfirmed-transactions` and the health check read only the+  page they return, newest first, instead of loading and sorting the whole+  mempool on every request. Data version 20: a version 18 or 19 database is+  migrated when it is opened, with no resync.+- The block store no longer reads the whole mempool to publish mempool+  events, to set the mempool gauge or to find expired transactions. Each+  import reports which transactions it added to the mempool or removed from+  it; pruning reads only the oldest entries of the index. Before, every+  block, every batch of new transactions and every clock tick (several per+  second) read the whole mempool at least once.+- Count the transactions of an xpub without building the list of all of them+  when the cache is not used.+- Keep the derived addresses of the 512 to 1024 most recently queried xpubs+  in memory. Every xpub query derived each address again before reading a+  balance. Addresses depend only on the xpub, so the cache never goes stale.+- Page the unspent outputs of an xpub by merging its addresses in order of+  block, stopping once the page is full, instead of decoding up to offset+  plus limit outputs of every address and sorting them all. The pages are+  the same.+- Count the transactions of an xpub by merging the raw keys of its+  addresses, without decoding keys or reading values.+- Open the iterators of a query over many addresses in one bracket instead+  of one bracket each. Every call into RocksDB walked the stack past one+  frame per address.+- Double the page, up to 1024 rows, when streaming a source by offset, as+  `/blockchain/unspent` does. The rows read and skipped grew with the square+  of the rows returned.+- Check that an xpub is cached in the same Redis round trip as the read of+  its balances, transaction count, or a page of its transactions or unspent+  outputs that does not start at a transaction.+- Serialize a transaction at most twice to compute its id, size and weight,+  and stop looking up its witnesses with a quadratic list index.+- Build on haskoin-core 1.6.1, which encodes CashAddr and Base58 addresses+  faster: about 10% less CPU per request on endpoints that return many+  addresses.+- The executable uses every core of the host by default (`-N` instead of+  `-N4`). Set `+RTS -N<n> -RTS` on the command line to choose a number.+- Read the balances of many addresses, and lists of transactions, with one+  RocksDB multi-get instead of one read per key. `StoreReadBase` has a new+  method, `getManyTxData`, which defaults to reading the transactions one by+  one; `getTransactions` reads many transactions with it.+- Give RocksDB a block cache of 1 GiB by default, shared by all column+  families, and keep their index and filter blocks in it at high priority,+  with those of level 0 pinned. Before, each column family had RocksDB's+  default cache of 32 MiB, and index and filter blocks stayed in memory+  outside any cache for every open file. Set the size with `--block-cache-mb`+  or `BLOCK_CACHE_MB`; 0 restores the RocksDB defaults. `StoreConfig` and+  `withDatabaseReader` take the size in bytes. Requires rocksdb-haskell-jprupp+  2.6.0.++### Added++- `store/bench`: a benchmark server that syncs a synthetic regtest chain and+  serves the web API on it, and times the encoding of a response with the+  argument `micro`. `scripts/bench-web.sh` runs wrk against it and reports+  the CPU time of the server per request.++## [1.7.0] - 2026-11-06++### Changed++- Build on haskoin-core 1.6.0 and haskoin-node 2.3.0, the releases with the+  fixes of the November 2026 security review. Their changelogs list the+  changes to message parsing, header validation and peer handling that the+  store now runs with.+- An output has an address only when the network defines one for its script,+  as in haskoin-core 1.6.0. A SegWit-shaped output has no address on Bitcoin+  Cash. A 256-bit script hash output (`OP_HASH256`, a push of 32 bytes,+  `OP_EQUAL`) has none on Bitcoin. Such an output is no longer indexed under+  an address, and its `address` is `null`. Entries that an older version+  wrote under such an address stay in the database. No request can name them,+  since the address has no text form on the network.+- `POST /transactions` takes a body that holds one transaction and nothing+  else. Bytes after the transaction are refused, as Bitcoin Core does for a+  raw transaction. They used to be ignored.+- `POST /transactions` takes the transaction in the format that peers of the+  network read. A network without SegWit, such as Bitcoin Cash, has the+  format without witness only.+- `GET /peers` gives the `version` of a peer as a signed number, as+  haskoin-core 1.6.0 reads it. A peer that sends version ffffffff has version+  -1, where it used to appear as 4294967295.+- Breaking, in `haskoin-store-data`: `toTransaction` takes the `Network` as+  its first argument, and the `version` field of `PeerInfo` is an `Int32`.+  The binary form of `PeerInfo` is the same bytes as before.+- From haskoin-node 2.3.0: a peer is disconnected when it sends more than+  50000 entries in an `inv` or `getdata` message. On Bitcoin Cash a message+  may be at most 2 MiB, unless it carries block content. Transactions from+  Bitcoin Cash peers are read in the format without witness only.++### Fixed++- A response no longer fails on a transaction with an output that has no+  address on the network. Since haskoin-core 1.5.0 the JSON encoder of such+  an address threw, which cut the response off. One such output is in Bitcoin+  block 211997 (transaction+  `a4bfa8ab6435ae5f25dae9d89e4eb67dfa94283ca751f393c1ddc5a837bbc31b`,+  output 0).++## [1.6.5] - 2026-11-04++### Fixed++- Build on haskoin-core 1.5.1, which evaluates the ASERT difficulty rule at the+  parent block. With 1.5.0 the store rejected a valid block on Bitcoin Cash+  mainnet at height 975899.++## [1.6.4] - 2026-11-04++### Added++- Support Bitcoin testnet4 (`NET=btctest4`), from haskoin-core 1.4.0.+- Option `--block-window` (`BLOCK_WINDOW`) to set how many blocks to request+  from the syncing peer at a time.+- Option `--write-buffer-mb` (`WRITE_BUFFER_MB`) to cap RocksDB memtables+  across all column families, 256 MiB by default (0 = unlimited). Without a+  cap the nine column families can keep up to 1.1 GiB of memtables.+- Option `--max-wal-mb` (`MAX_WAL_MB`), 1024 MiB by default (0 = RocksDB+  default), to bound the RocksDB write-ahead log: once it is larger, the+  column families holding its oldest file are flushed so the file can be+  deleted.+- `/dbstats` starts with the memory RocksDB uses outside the GHC heap:+  memtables and table readers for each column family, and the block cache.+- The mempool is kept in its own column family, one key per transaction,+  instead of as a single list value that every import batch read, loaded+  into a hash table and wrote back whole. Data version 19: a version 18+  database is migrated when it is opened, with no resync.++### Changed++- Replace monad-logger with the fast-logger `Logger` from haskoin-node 2.0.0.+- Request 64 blocks at a time from the syncing peer instead of 500, and ask+  for more once fewer than 16 remain, to bound memory use while catching up.+- haskoin-store-data builds against secp256k1-haskell 2.0.0, which hides the+  raw `PubKey` constructor: `XPubSpec` reads its stored key with `pubKey`,+  which checks it.+- Build on haskoin-core 1.5.0, haskoin-node 2.1.2 and secp256k1-haskell 2.0.0.+  `genesisBlock` no longer takes a context.+- A pay-to-public-key output has no address any more, as in haskoin-core+  1.5.0. Its coins cannot be spent with the input script of the P2PKH address+  of the same key, so they are no longer counted under that address.+- The block store no longer reads the whole mempool four extra times on every+  internal ping: it compares mempool snapshots only when there are pending+  transactions to import or expired ones to delete.+- Run with four capabilities (`-N4`) instead of one per core (`-N`), so that+  memory overhead does not grow with the size of the host. Override with+  `+RTS -N<n> -RTS`.+- Docker image: pin the base image to Debian trixie (RocksDB 9.10) by digest+  instead of `debian:latest`, and run with `MALLOC_ARENA_MAX=2` to limit+  malloc fragmentation from RocksDB.+- Each `/events` client, websocket or HTTP stream, gets a queue of at most+  10,000 events and is disconnected when it fills up, instead of the queue+  growing for as long as the client stays connected. Store events that+  `/events` does not show are no longer queued for its clients.+- A request waiting for another to load an xpub into the cache retries for+  about 30 seconds, with at most 5 seconds between attempts, and then serves+  the xpub from the database. It used to retry forever with a delay that+  doubled each time.++- Continuous integration on GitHub Actions: build, run both test suites,+  and check that the heap stays flat while syncing 6000 regtest blocks.++### Fixed++- Support full-RBF: accept replacements of transactions that do not signal BIP125.+- Correct various space leaks.+- `Unspent` implements `coinInputKind`, new in haskoin-core 1.5.0, from its+  output script, so coin selection with it no longer fails at run time.++### Added++- Support for testnet4 on both networks.++## [1.6.3] - 2026-08-31++### Added++- Allow disabling web server to sync block chain only.++## [1.6.2] - 2026-08-13++### Fixed++- Fix upstream regressions in haskoin-node.++## [1.6.1] - 2026-08-13++### Fixed++- Support latest version of haskoin-node.++## [1.6.0] - 2026-08-12++### Fixed++- Support for Bitcoin Cash 256-bit scripts.+- Address parsing now allows only correct lengths.++## [1.5.16] - 2025-05-08++### Changed++- Move to personal repository.++### Removed++- Disable `NO_SLOW` as an option.++### Added++- Allow to set `NO_XPUB` to disable extended public keys.++## [1.5.14] - 2024-12-04++### Changed++- Move `/dbstats` and `/mempool` to slow endpoints section.++### Added++- Cull old transactions from mempool automatically.++## [1.5.13] - 2024-10-25++### Changed++- Dummy release: update upstream LTS Haskell dependency in `stack.yaml` file.++## [1.5.12] - 2024-07-22++### Added++- Metric to track time to import a new block.+- Parameter `no_compact` allowed for backwards compatiblity.++## [1.5.11] - 2024-05-24++### Fixed++- Parameter `noCompact` corrected for `multiaddr` endpoint.++## [1.5.10] - 2024-05-24++### Fixed++- Prune obsolete transactions from outgoing peer requests.+- Remove unnecessary module imports.++## [1.5.9] - 2024-05-23++### Fixed++- Only sync mempool once for each peer.++## [1.5.8] - 2024-05-23++### Fixed++- Dummy release to bump upstream dependency on `haskoin-node`.++## [1.5.7] - 2024-05-23++### Fixed++- Dummy release to bump upstream dependency on `statsd-rupp`.++## [1.5.6] - 2024-05-22++### Changed++- Allow to toggle RocksDB Bloom filters off.++### Fixed++- Correct command-line option behaviour for some rarely-used parametres.++## [1.5.5] - 2024-05-19++### Changed++- Lock Redis before checking if the database needs to be pruned, rather than after.+- Lock xpub in Redis to avoid computing derivations multiple times.++### Fixed++- Rename redis-min-gap to redis-min-addrs to match environment config variable.++## [1.5.4] - 2024-05-07++### Fixed++- Fix Redis bug where old keys weren't pruned quickly enough.++### Changed++- Remove Nomad-specific environment variable parsing for statsd.++## [1.5.2] - 2024-04-24++### Changed++- Use default GHC garbage collector.++## [1.5.1] - 2024-04-23++### Changed++- Compile with GHC 9.8.2.+- Enable optimisations.++## [1.5.0] - 2024-04-06++### Fixed++- Use an optimised topological sort for importing transactions in bulk.++## [1.4.0] - 2024-03-27++### Changed++- Use default optimisation settings.+- Use default RocksDB parameters.++## [1.3.0] - 2024-03-14++### Changed++- Bump upstream dependencies.++## [1.2.5] - 2023-01-31++### Fixed++- Test suite now working again.++## [1.2.4] - 2023-01-31++### Changed++- Bump upstream dependencies.++## [1.2.3] - 2023-12-06++### Changed++- Transactions received via POST are just forwarded to every peer without delay or validation.+- Bump dependency on NQE to resolve async issue.++## [1.2.2] - 2023-10-21++### Fixed++- Fix comparison function for unspent outputs.++### Changed++- Avoid deprecated `param` function from Scotty.++## [1.2.1] - 2023-10-04++### Fixed++- Correct web exception output format (JSON).+- Correct block path parsing order.++## [1.2.0] - 2023-10-03++### Changed++- Upgrade `scotty` dependency and fix incompatibilities.++## [1.1.0] - 2023-09-15++### Added++- Add cache height stats.+- Add database iterator stats.++### Changed++- Detect xpub gap more strictly.+- Use statsd-rupp instead of ekg-stats.+- All stat reporting keys changed.++### Removed++- Remove unsafeInterleaveIO from Web.hs.+- Remove unnecessary WAI middlewares.+- Remove server and client error reporting per endpoint.++## [1.0.2] - 2023-08-25++### Changed++- Code refactoring and simplification.+- Improve logging output.++### Removed++- Do not impose time limits on REST API responses.++### Fixed++- Remove loop in Blockchain.info unspents function.+- Correct documentation for /blockchain/rawblock endpoint.+- Fix no match in record selector address for coinbases.++## [1.0.1] - 2023-08-03++### Changed++- Add compatibility with latest nightly LTS Haskell.++## [1.0.0] - 2023-07-28++### Changed++- Compatibility with latest haskoin-core and haskoin-node packages.+- Use DuplicateRecordFields and OverloadedRecordDot language extensions.+- Modernize package in a backwards-incompatible manner.+- Refactor package extensively.++## [0.65.10] - 2023-05-12++### Changed++- Revert to older LTS Haskell to attempt to resolve memory leak.++### Removed++- Remove unnecessary lock release.++## [0.65.9] - 2023-05-09++### Changed++- Be more aggressive caching individual transactions.+- Introduce a periodic cache mempool sync task.++## [0.65.8] - 2023-05-08++### Changed++- Perform health check in separate thread.++## [0.65.7] - 2023-05-05++### Changed++- Do not sync mempool to Redis if node is not up-to-date.++## [0.65.6] - 2023-05-04++### Changed++- Improve caching ingestion performance.++## [0.65.5] - 2022-09-19++### Added++- Fee field added to Blockchain.info-style export-history endpoint.++### Changed++- Bump version due to Git mistake.++## [0.65.3] - 2022-04-13++### Added++- IPv6 peer support.++## [0.65.2] - 2022-04-13++### Fixed++- Correct lazy bytestring deserialization algorithm for web client.++## [0.65.1] - 2022-04-13++### Changed++- Simplify function in Cache module.+- Bump depenedencies.++## [0.65.0] - 2022-02-03++### Changed++- Spenders now embedded into tx objects in database for performance.+- Use faster IORef instead of slow TVar when ingesting.+- Use mutable hash tables when ingesting.+- Allow to disable unscalable API endpoints.++## [0.64.19] - 2022-02-01++### Added++- Allow to set query timeouts.++## [0.64.18] - 2022-01-21++### Fixed++- Off-by-one error when importing change addresses to cache.++## [0.64.17] - 2022-01-21++### Fixed++- Add change addresses to cache when necessary.++## [0.64.16] - 2022-01-10++### Fixed++- Correct fee calculation for blocks that do not claim all rewards.++## [0.64.15] - 2022-01-07++### Fixed++- Correct scale of cache pruning code.++## [0.64.14] - 2022-01-07++### Fixed++- Always prune to 80% of max cache size.++## [0.64.13] - 2022-01-07++### Fixed++- Avoid doing a hissy fit on transient cache issue.++## [0.64.12] - 2022-01-07++### Fixed++- Prune a little more cache to allow for new keys.++## [0.64.11] - 2022-01-06++### Fixed++- Prune cache more often.++## [0.64.10] - 2021-12-30++### Changed++- Return empty addresses in xpub balances call.++## [0.64.9] - 2021-12-29++### Fixed++- Avoid notifying same transactions in mempool.++## [0.64.8] - 2021-12-26++### Changed++- Use better Fourmolu format.+- Be more precise when counting retrieved items.++### Fixed++- Avoid transactions duplicated in xpub data.+- Refactor some functions to make them more robust.++## [0.64.7] - 2021-12-26++### Fixed++- Optimisations for various Blockchain endpoints.+- More accurate item counts for various web endpoints.++## [0.64.6] - 2021-12-25++### Fixed++- Record cache index time just once per xpub.++## [0.64.5] - 2021-12-25++### Changed++- Add suffix cached to cache entries to avoid conflicts with data entries.++## [0.64.4] - 2021-12-25++### Fixed+- Bring dots back and avoid stats hacks.++## [0.64.3] - 2021-12-25++### Fixed++- Fix typo.++## [0.64.2] - 2021-12-25++### Changed++- Replace dots for underscores in metrics.++## [0.64.1] - 2021-12-25++### Changed++- Automatic formatting.+- Improve cache debug logging.++### Fixed++- Add item count to 'all' query metrics.++## [0.64.0] - 2021-12-25++### Changed++- Improve metrics.++### Fixed++- Get lock before importing transactions to cache.++## 0.63.0+### Fixed+- Make cache updates lightweight.++## 0.62.1+### Fixed+- Fix idempotent transaction elimination bug.++## 0.62.0+### Added+- Configurable Blockchain price URLs.+- Reuse HTTP connections with wreq sessions.++## 0.61.1+### Changed+- Upstream improvements on Base58 performance.++## 0.61.0+### Fixed+- Fixed recursion bug in Blockchain format conversion function.++## 0.60.0+### Fixed+- Correct notifications subsystem.++## 0.59.0+### Fixed+- Correct internal dependency.++## 0.58.0+### Fixed+- Correct output freeing algorithm.++## 0.57.0+### Changed+- Report errors freeing outputs.++## 0.56.0+### Changed+- Report all deleted mempool transactions.+- Crash on certain data corruption.++## 0.55.0+### Changed+- Always have a previous output object in Blockchain inputs.++## 0.54.0+### Added+- WebSocket support.+- Testnet4 support.++## 0.53.11+### Changed+- Dummy version increase to signal upstream update of Haskoin Core.++## 0.53.10+### Fixed+- Correct test that was reversed in previous version.++## 0.53.9+### Removed+- Mempool is no longer synced by default to fix public Bitcoin Core regression.++## 0.53.8+### Changed+- Put derivations stat inside database.++## 0.53.7+### Added+- Added counters for database retrievals.+- Added counter for xpub derivations.++### Changed+- Removed some buggy or unnecessary stats.++## 0.53.6+### Changed+- Improve web server statistics.++## 0.53.5+### Addded+- Endpoint to delete an xpub from cache.++## 0.53.4+### Added+- Debugging when reverting block.++### Fixed+- Negative (headers - blocks) diff shown incorrectly.+- Infinite loop when removing a block's coinbase transaction.++## 0.53.3+### Added+- Blockchain.info endpoint: `q/pubkeyhash/:addr`.+- Blockchain.info endpoint: `q/getsentbyaddress/:addr`.+- Blockchain.info endpoint: `q/pubkeyaddr/:addr`.+- Configurable request body limit size.++## 0.53.2+### Fixed+- Return output in satoshi for some endpoints.++## 0.53.1+### Fixed+- Return 404 in case of bad address.++## 0.53.0+### Added+- Blockchain.info endpoint: `q/addresstohash/:addr`.+- Blockchain.info endpoint: `q/addrpubkey/:pubkey`.+- Blockchain.info endpoint: `q/hashpubkey/:pubkey`.+- Blockchain.info endpoint: `q/getblockcount`.+- Blockchain.info endpoint: `q/latesthash`.+- Blockchain.info endpoint: `q/bcperblock`.+- Blockchain.info endpoint: `q/txtotalbtcoutput/:txid`.+- Blockchain.info endpoint: `q/txtotalbtcinput/:txid`.+- Blockchain.info endpoint: `q/txfee/:txid`.+- Blockchain.info endpoint: `q/txresult/:txhash/:addr`.+- Blockchain.info endpoint: `q/getreceivedbyaddress/:addr`.+- Blockchain.info endpoint: `q/addressbalance/:addr`.+- Blockchain.info endpoint: `q/addressfirstseen/:addr`.+- Support xpubs in `blockchain/rawaddr` endpoint.++### Fixed+- Various latest blocks endpoint fixes.+- Allow empty parameters in Blockchain.info POST requests.++## 0.52.13+### Fixed+- Upstream fixes for unknown inv types.++## 0.52.12+### Fixed+- Fix limits for legacy endpoints.++## 0.52.11+### Added+- Alias for Blockchain.info raw address endpoint.++### Fixed+- Fix unknown message types without payload upstream.+- Randomise peer timeouts upstream.++## 0.52.10+### Fixed+- Fix binary search algorithm for blocks upstream.++## 0.52.9+### Added+- Add Blockchain.info latest block endpoint.+- Add Blockchain.info unconfirmed-transactions endpoint.+- Add Blockchain.info blocks for one day endpoint.++## 0.52.8+### Fixed+- Fix special case for zero limit.++## 0.52.7+### Fixed+- Fix cache bug when too many transactions in mempool.++## 0.52.6+### Changed+- Use rationals for Blockchain.info export-history endpoint arithmetics.++## 0.52.5+### Added+- Support for Blockchain.info export-history endpoint.++### Fixed+- Use strict bytestring decoder for HTTP API client.++## 0.52.4+### Fixed+- Use Base58 addresses by default in Blockchain.info balance querios.++## 0.52.3+### Fixed+- Errors should also contain CORS headers.++## 0.52.2+### Fixed+- Blockchain.info raw address limit should use `limit` parametre and not `n`.++## 0.52.1+### Changed+- Stream multiple transactions when requested.++### Fixed+- Fix missing unspent outputs.++## 0.52.0+### Changed+- Remove unnecessary performance optimisations.++### Fixed+- Reduce balance retrievals for xpubs.+- Hack UTXO ordering to avoid unwanted behaviours.+- Do not merge conduits when there are too many.++## 0.51.0+### Fixed+- Data type for RequestTooLarge was missing.++## 0.50.3+### Added+- Log HTTP request bodies.++### Removed+- Remove web request timeout.++### Fixed+- Do not ignore unknown start tx anchors in conduits.+- Use fast transaction counting in cache.++### Changed+- Use WAI middlewares to manage stats.++## 0.50.2+### Fixed+- Do not use lazy parsers for incoming data that may throw exceptions.++## 0.50.1+### Fixed+- Do not allow incoming POST requests of unlimited body size.++## 0.50.0+### Added+- Limit number of simultaneous xpubs being cached.++## 0.49.0+### Changed+- Improve conduit merging algorithm.++## 0.48.0+### Added+- Timeouts and token bucket for web requests.+- Configurable price fetch interval.++## 0.47.6+### Fixed+- Fix web.all stat.++## 0.47.5+### Changed+- Simplify stats.++## 0.47.4+### Added+- Implement Blockchain.info block height.+- Implement Blockchain.info raw address.+- Implement Blockchain.info balances.++### Fixed+- Make Blockchain.info unspent endpoint more efficient.+- Make Blockchain.info xpubs endpoint more efficient.++### Changed+- Use compact JSON serialization by default everywhere.++## 0.47.3+### Fixed+- Fix serialization bug with unspents.++## 0.47.2+### Fixed+- Fix serialization bug for health check.++## 0.47.1+### Fixed+- Fix serialization bugs for web data types.++## 0.47.0+### Added+- Support for legacy block endpoints.++### Changed+- Serialization now uses bytes library and does interleaved IO.++## 0.46.6+### Added+- Allow comma-separated values in multiaddr.++## 0.46.5+### Changed+- Throw error when offset exceeded instead of truncating it.++## 0.46.4+### Fixed+- Do not compute balances separately for loose addresses when part of an xpub.++### Changed+- Do not fetch entire transaction list for loose addresses.+- Transaction count in wallet object is just a synonym for filtered tx count.+- Use conduits to prevent over-fetching transactions from database.++## 0.46.3+### Fixed+- Make hex value encoding compatible with Java BigInteger decoder.++## 0.46.2+### Changed+- Allow negative confirmations for compatibility with old API.++## 0.46.1+### Added+- Filters for transactions in multiaddr endpoint.++## 0.46.0+### Fixed+- Release to fix unintentional upload.++## 0.45.0+### Added+- Using a tx_index that results in a txid conflict returns a 409 - Conflict.++## 0.44.0+### Changed+- Numeric txid now 53 bits long and doesn't return transactions when hashes collide.++## 0.43.1+### Fixed+- Do not overcount statistics.++## 0.43.0+### Changed+- Transaction index now simply encodes txid as a long number (sorry JS users).++## 0.42.4+### Changed+- Optimise encoding of JSON data for Blockchain.info API.++## 0.42.3+### Changed+- Narrow down the balance for transactions to onlyShow set.++## 0.42.2+### Changed+- Make unspent algorithm more efficient.++## 0.42.1+### Added+- Support for Blockchain.info API unspent endpoint.++### Fixed+- Fix RBF transactions on Blockchain.info API.++## 0.42.0+### Removed+- No more pruning of old mempool transactions.++## 0.41.4+### Fixed+- Correct error computing wallet balances.++## 0.41.3+### Changed+- Use whole active set when computing results for multiaddr.+- Compute correct count of transactions for wallet.++## 0.41.2+### Changed+- Caching mempool is not much slower when pruning, so reversing that change.++## 0.41.1+### Changed+- Only prune mempool every ten minutes or so.++## 0.41.0+### Removed+- No more cache eviction endpoint.++## 0.40.22+### Added+- More metrics for cache.++## 0.40.21+### Added+- Support HTTP GET for multiaddr endpoint.++## 0.40.20+### Added+- Prefix 'app.' in statsd when using Nomad variables.++## 0.40.19+### Added+- Statsd support and pleny of statistics.++### Removed+- Removed use of sync lock for importing xpubs in cache due to volume.++## 0.40.18+### Added+- New options to control cache lock retry count and delay.++### Changed+- Cache xpubs asyncrhonously.+- Mempool syncs fully every refresh cycle.++## 0.40.17+### Changed+- Delete more than 1000 xpubs from Redis if needed.+- Attempt to get lock ten times in order to index a new xpub.++## 0.40.16+### Fixed+- Fix best block not set.++## 0.40.15+### Fixed+- Do not update cache best block if it is higher.++## 0.40.14+### Fixed+- Always respond to cache pings to avoid getting stuck.++## 0.40.13+### Added+- Be more careful about importing blocks to the cache.++### Removed+- Reduce excessive cache logging.++## 0.40.12+### Fixed+- Correct bad arithmetics in cache cooldown.++## 0.40.11+### Added+- Debug log cache cooldown.++### Fixed+- Unlimited retry lock acquisition now works.+- Make locking tighter++## 0.40.10+### Changed+- Reduce the number of things that cache do when pinged.++## 0.40.9+### Fixed+- Avoid filling the cache actor mailbox with pings.++## 0.40.8+### Fixed+- Correct a typo in cache code.++## 0.40.7+### Changed+- Improve cache mempool sync algorithm.++## 0.40.6+### Changed+- Make cache cooldown a number.++## 0.40.5+### Added+- Log every non-successful query.++## 0.40.4+### Fixed+- Use Redis TTL for cache cooldown.++## 0.40.3+### Fixed+- Mempool no longer gets saved in cache when node is not in sync.++## 0.40.2+### Changed+- Change source of price data to https://blockchain.info/ticker.++## 0.40.1+### Removed+- Do not expose Paths_haskoin_store module explicitly.++## 0.40.0+### Added+- Blockchain.info API compatibility for multiaddr and rawtx endpoints.++### Changed+- Segwit fields are no longer omitted on incompatible networks.++## 0.39.0+### Changed+- Upgrade Haskoin Core to support higher version on witness addresses.++## 0.38.4+### Fixed+- Fix block search by timestamp.++## 0.38.3+### Changed+- Disable low latency garbage collector by default.++## 0.38.2+### Changed+- Use GHC 8.10 and its low latency garbage collector.++## 0.38.1+### Changed+- Optimise algorithm for fork safety check.++## 0.38.0+### Added+- Support for Bitcoin Cash hard fork.++## 0.37.5+### Added+- Configurable cache refresh time.++## 0.37.4+### Fixed+- Now request mempool when connecting even if already in sync.++### Changed+- Add a delay to mempool sync to allow for block headers to catch up.+- Attempt to sync mempool whenever syncing is done or a new peer connects.++## 0.37.3+### Changed+- Correct logic error when deleting superseded transactions.++## 0.37.2+### Changed+- Simplify multiple algorithms to hopefully solve corruption bug.+- Allow to disable mempool transaction indexing via command line.++## 0.37.1+### Changed+- Better locking around caching operations.+- Have a global cooldown on cache mempool updates.++### Fixed+- Fix address unspent retrieval using correct column family.++## 0.37.0+### Added+- Add support for column families and prefixes breaking database backwards compatibility.++## 0.36.2+### Fixed+- Fix another place where mempool sorting was wrong.++## 0.36.1+### Fixed+- Fix wrong sorting of mempool transactions in database.++## 0.36.0+### Changed+- Do not use STM transactions because it is impossible to reason about preloading.+- Make importing blocks after mempool is populated faster.++## 0.35.2+### Changed+- Reduce frequency of cache mempool updates.++## 0.35.1+### Changed+- Remove cache lock retries.+- Do not use lock when caching a new xpub.++## 0.35.0+### Changed+- Use new RocksDB Haskell bindings.+- Make headers - blocks diff configurable in health check.+- Simplify locking in cache.++## 0.34.8+### Changed+- Minor refactoring.+- Only send one mempool message ever.++## 0.34.7+### Changed+- Fix block heights endpoint URL.++## 0.34.6+### Removed+- Remove asynchronous database retrieval permanently due to instability.++## 0.34.5+### Added+- Preload memory before freeing outputs.++## 0.34.4+### Added+- Asynchronous preloading only for blocks.++## 0.34.3+### Removed+- Disable asynchronous preloading to prevent segmentation fault error.++## 0.34.2+### Removed+- No more upper limit for mempool call.++### Fixed+- Health check uses minimum between last block and mempool tx.++## 0.34.1+### Added+- Pending mempool transactions health check.++### Removed+- No more upper limit for mempool call.++## 0.34.0+### Changed+- New format for health check.++## 0.33.1+### Changed+- Upload with updated Cabal files.++## 0.33.0+### Fixed+- Fix outputs showing up as both spent and unspent.++### Changed+- Simplify data structures used to hold uncommitted changes.+- Split StoreRead class in two.+- Improve pre-loading algorithms.++## 0.32.3+### Fixed+- Fix command line argument mappings to env vars.++## 0.32.2+### Fixed+- Do not complain when deleting non-existent unconfirmed txs.++## 0.32.1+### Fixed+- Fix corruption bug when deleting transactions during some reorgs.+- Fix tests on data package.+- Rename upstream address string functions.++## 0.32.0+### Changed+- Concurrently load dependencies when importing transaction data.+- Import each transaction using STM.+- Rework logging.+- Refactor all block chain importing algorithms.+- Use peer locking when importing data.+- Change license to MIT.++### Added+- Make max peer count configurable.++### Fixed+- Prevent premature peer timeouts when importing blocks.++## 0.31.0+### Added+- Add Haskell web client code to data library.+- Use type-safe definitions for web paths and parameters.+- Support JSON pretty encoding and enable by default for some endpoints.+- Allow environment variables for configuration.++### Changed+- Change command-line arguments to better fit GNU style.+- Refactor code to manipulate address balances.+- Improve logging.+- Rate-limit mempool importing.+- Do not add xpubs to cache if node not in sync.+- Do not import transactions to cache if no xpubs in cache.+- Refactor transaction importing logic extremely.+- Refactor memory database and writer.++### Fixed+- Fix BTC double-count transaction bug.+- Add missing xpub derive parameter to xpub evict documentation.++## 0.30.1+### Changed+- Limit mempool API endpoint list.++## 0.30.0+### Changed+- Raw JSON values are now wrapped in an object with a result property.+- Better logging when importing transactions into mempool.++### Fixed+- Transaction limit by block height works now.++## 0.29.3+### Changed+- Revert concurrent retrieval changes from 0.29.1 and 0.29.0 to solve stability+  issues.+- Bitcoin Cash (BCH) replaces BTC as default network in CLI.+- Peer discovery enabled automatically if no peers specified in CLI.+- Use separate Stack configuration file for data package.++## 0.29.2+### Changed+- Increase timeouts for manager and chain health checks.++## 0.29.1+### Added+- Perform xpub operations concurrently.++## 0.29.0+### Changed+- BlockTx renamed to TxRef.+- Remove unnecessary monad transformers to simplify import code.++### Added+- Retrieve balances and unspent outputs concurrently when importing.+- Coin instance for Unspent in data package.++## 0.28.0+### Changed+- Change the way that limits, start, and offset values are handled.++### Fixed+- Fix incorrect limit responses.++## 0.27.0+### Added+- Complete support for offset.++### Changed+- Improve monad stack for cache.++## 0.26.6+### Added+- Support for offset in all endpoints where it makes sense.++## 0.26.5+### Fixed+- Sort after using the nub function or use Data.List.nub in exceptional cases.++## 0.26.4+### Changed+- Don't do certain validity checks on confirmed transactions.++### Fixed+- Do not crash when syncing the blockchain from scratch on a populated cache.++## 0.26.3+### Changed+- Replace nub function everywhere with faster version written using hash sets.++### Fixed+- Fix shared cache locking.++## 0.26.2+### Changed+- Simplify cache block import code.+- Improve cache import performance.++## 0.26.1+### Fixed+- Do not call zadd or zrem in Redis with incorrect argument count.++### Changed+- Do not use slow Transaction type in Logic.+- Improve logging while importing blocks.+- Better transaction import algorithm.++## 0.26.0+### Changed+- Add information to some binary types for feature parity with JSON.++## 0.25.4+### Changed+- Hide segwit data on non-segwit networks.++### Fixed+- Fix work which now always serialises as a number literal.++## 0.25.3+### Changed+- Fix version display on health check.++## 0.25.2+### Changed+- Fix wrong version of Haskoin Store Data dependency.++## 0.25.1+### Changed+- Improve code organisation.+- Split data definitions and serialisation into its own package.++## 0.24.0+### Changed+- Use mocking to simulate peers instead of connecting to the network while testing.+- Depend on latest version of Haskoin Core for faster JSON serialisation.+- Clean up confusing JSON encoding/decoding codebase.++## 0.23.24+### Changed+- Implement faster JSON encoding using toEncoding from the Aeson library.++## 0.23.23+### Changed+- Do not read full mempool so often from cache code.++## 0.23.22+### Changed+- Depend on latest Haskoin Node that fixes debugging regression.++## 0.23.21+### Changed+- Reduce non-debug log verbosity.++## 0.23.20+### Changed+- Better Haskoin Node logging.++## 0.23.19+### Changed+- Use set peer timeout instead of constant in block store. ++## 0.23.18+### Changed+- Allow setting peer timeouts in command line.++### Fixed+- Bump Haskoin Node dependency to fix another premature timeout condition.++## 0.23.17+### Fixed+- Touch syncing peer whenever we process one of its blocks to avoid premature timeout.++## 0.23.16+### Changed+- Better algorithm to avoid importing transaction multiple times in cache.+- Depend on latest Haskoin Node.++## 0.23.15+### Changed+- Do not storm the cache with mempool transactions.++## 0.23.13+### Fixed+- Do not ignore deleted incoming transactions.++## 0.23.12+### Fixed+- Do not do RBF checks when replacing a mempool transaction with a confirmed one.++## 0.23.11+### Fixed+- Streamline mempool transaction importing.++## 0.23.10+### Fixed+- Fix transactions not recorded in cache mempool set.+- Fix transactions being downloaded multiple times.++### Removed+- Do not store orphan transactions in database.++### Changed+- Use sets for incoming transactions instead of lists.+- Do not do anything to the cache if there are no xpubs in it.++## 0.23.9+### Fixed+- Wiping mempool fixed.++## 0.23.8+### Added+- Chunk transactions to be deleted from mempool.++## 0.23.7+### Added+- Mempool improvements.++## 0.23.6+### Added+- Ability to wipe mempool at start.+- Improvements to mempool processing code.++## 0.23.5+### Changed+- Tighten the locking loop to avoid slow cache building.++## 0.23.5+### Fixed+- Wrong error in cache when acquiring lock.++## 0.23.4+### Added+- Add extra debug logging for cache code.++### Fixed+- Fix a bug with xpub growing algorithm.++### Changed+- Use locks instead of transactions to update cache.++## 0.23.3+### Fixed+- Reduce contention when many instances of Haskoin Store share a cache.++## 0.23.2+### Added+- Allow retrieving xpub data without using cache.++## 0.23.1+### Added+- Allow xpub eviction from cache via API.+- Clarify cache address addition code.++### Fixed+- Balances were incorrectly computed in cache when new transactions arrived.++## 0.23.0+### Added+- Support for Redis transactions.+- Use a smaller initial gap for empty xpubs.++### Changed+- Remove custom JSON encoding class.+- Refactor and code simplification.++## 0.22.5+### Fixed+- Cache was being completely pruned.++## 0.22.4+### Fixed+- Cache now prunes correctly.++## 0.22.3+### Fixed+- Bug was making cache get stuck when pruning.++## 0.22.2+### Changed+- More efficient algorithms for caching and cache misses.+- Better debug logging of cache hits and misses.++## 0.22.1+### Added+- More debug logging for cache hits.++### Fixed+- Bug using maximum against empty list.++## 0.22.0+### Changed+- Extreme code refactoring.+- Move all code to Haskoin and drop Network from modules.++### Added+- Use Redis pipelining when importing multiple transactions into cache.+- Implement configurable LRU for Redis cache.+- Import xpubs directly into cache from web worker thread when a key is requested.++### Removed+- Only expose a few modules to external API.++## 0.21.7+### Changed+- Improve build configuration.++### Fixed+- Use minimum used addresses instead of minimum index for xpub cache decision.++## 0.21.6+### Fixed+- Fix missing xpub unspent outputs when using cache.++## 0.21.5+### Added+- Only store xpubs in cache if they have more than a threshold addresses used.++## 0.21.4+### Fixed+- Fix shared cache case where head is set beyond header chain by another node.++## 0.21.3+### Fixed+- Fix bug where best head was not being registered in cache.+- Fix best head in cache being decoded incorrectly.++## 0.21.2+### Added+- Complete support for Redis xpub cache.++## 0.21.1+### Fixed+- Latest version of secp256k1-haskell works with Debian 9.++## 0.21.0+### Fixed+- Fix output of web API calls when issuing limits with offsets.++### Changed+- Massive refactoring of entire codebase.++### Added+- Work in progress Redis caching for extended public keys.++## 0.20.2+### Changed+- Filter xpub address balances on web API to show only addresses that have been used.++### Removed+- Remove paths and addresses from xpub summary.++## 0.20.1+### Changed+- Refactor code greatly.+- Depend on new Haskoin Store package to avoid missing tx broadcasts.+- Merge StoreStream class into StoreRead.+- Move former streaming functions to use lists instead of conduits.+- Remove excessively verbose debugging.++## 0.20.0+### Added+- Set minimum version bounds for some dependencies.+- Now compatible with GHC 8.8.+- Extended key caching system using Redis.++### Changed+- Massively refactored codebase.+- Less verbose debug logging.++### Removed+- Removed conduits for faster queries.++## 0.19.6+### Added+- Use a parallel strategy to compute key derivations.++### Fixed+- Do not fail health check upon transaction timeout while syncing.++### Changed+- Do not use conduits for xpub balance streams.+- Multiple minor refactorings.++## 0.19.5+### Changed+- Minor refactor to block import code.++### Fixed+- Minor fix to transaction timeout check.++## 0.19.4+### Fixed+- Clarify and correct health check algorithm.++## 0.19.3+### Changed+- Add address transactions to cache.+- Improve multi-address transaction retrieval algorithms.++## 0.19.2+### Removed+- Cache-Control header turned out to be unnecessary.++### Fixed+- Fix some minor errors in web module.++## 0.19.1+### Added+- Set Cache-Control header to no-cache.++## 0.19.0+### Changed+- Store mempool in single key/value pair.++## 0.18.11+### Changed+- Do not stream mempool.++## 0.18.10+### Removed+- Disable timeout checks for testnets.++## 0.18.9+### Added+- Endpoint to locate a block by unix timestamp.++### Removed+- No more persistence for peers due to dependency on newest haskoin-node.++## 0.18.8+### Added+- Transaction and block timeouts for health check.+- Raw blocks.++## 0.18.7+### Fixed+- Missing tranasctions on xpub listings.++## 0.18.6+### Removed+- Follow Stack advise removing `-O2` GHC option.++## 0.18.5+### Added+- Compatibility with SegWit on extended public key endpoints.++### Changed+- Fix syncing peer not reset after timeout.+- Use simpler monad for streaming data.++## 0.18.4+### Changed+- Bump Haskoin Node to fix peers not stored and excessively verbose logging.++## 0.18.3+### Changed+- Configurable HTTP request logging. Disabled by default.++## 0.18.2+### Changed+- Fix for memory leak.++## 0.18.1+### Removed+- Remove transaction count from xpub summary object.++## 0.18.0+### Changed+- Simplified limits and start point handling on web API.+- Made transaction streaming algorithm faster for xpub transactions.+- Extended public key summary output contains all addresses that have received transactions.++### Added+- Fine-grained control for maximum limits via command line options.+- Transaction hash as starting point.+- Block hash as starting point.+- Timestamp as starting point.+- Configurable xpub gap limit.+- Transaction count added to xpub summary.+- UTXO count added to xpub summary.++### Removed+- Mempool endpoint now has no limits or offsets and always returns full list.+- Extended public key summary output no longer includes any transactions.+- Offsets not allowed for transaction lists involving multiple addresses or xpubs.+- Confusing block position parameter no longer part of web API.++## 0.17.2+### Changed+- Stream address balances and unspent outputs.+- Add configurable max value for limit and offset which defaults at 10000.++## 0.17.1+### Changed+- When posting a transaction to the network, timeout is now five seconds.+- Improve error message when transaction post timeout reached.+- Remove obsolete `not found` error for transaction post.+- Endpoints for retrieving blocks now do streaming for better performance.+- Improve Swagger API documentation.++## 0.17.0+### Added+- Endpoints for retrieving block transactions.+- Endpoint for retrieving set of latest blocks.++### Changed+- Use standardized JSON and binary serialization schemes for raw transaction endpoints.++## 0.16.6+### Changed+- Now logging info messages too by default.+- Consolidated web logging in middleware.++### Removed+- UUIDs for web requests.++## 0.16.5+### Removed+- Remove concurrency from xpub balance requests to prevent RocksDB segfaults.++## 0.16.4+### Removed+- Remove concurrency from requests using iterators to prevent RocksDB from segfaulting.++## 0.16.3+### Added+- Debugging information for web API.++## 0.16.2+### Changed+- Debugging disabled by default (use `--debug` to enable).++## 0.16.1+### Added+- Cache mempool transactions.+- Improve initial syncing performance.++## 0.16.0+### Added+- Orphan transaction support.+- Full address balance cache in RocksDB.+- Full unspent output cache in RocksDB.++### Changed+- Significantly refactor code.+- Move web stuff to its own module.+- Change types related to databases.+- Make xpub balance, transaction and unspent queries fetch data in parallel.++## 0.15.2+### Added+- Internal data types to support orphan transactions.++### Changed+- Do not spam block actor with pings.+- Fix balance/unspent cache not reverting when importing fails.+- Fix transaction sorting algorithm not including transaction position information.+- Fix conflicting mempool transaction preventing block from importing.++## 0.15.1+### Changed+- Fix duplicate coinbase transaction id bug.++## 0.15.0+### Removed+- Removed `PreciseUnixTime` data type.++### Changed+- Use 64 bits for Unix time representation.+- Data model now uses simplified Unix time representation.++## 0.14.9+### Added+- Last external/change index information to xpub summary object.++## 0.14.8+### Added+- Endpoint for xpub summaries.+- Endpoints for full transactions.+- Ability to query by offset/limit.+- More API documentation.++## 0.14.7+### Added+- Debug information for `publishTx`.++### Changed+- Transaction publishing no longer requests mempool.+- Fixed serialization for `TxId` freezing the entire program by infinitely recursing.++## 0.14.6+### Changed+- Enable full threading again as it was deemed not responsible for the freezing behaviour.++## 0.14.5+### Changed+- Enable threading but leave it at a single thread to be able to open more than 1024 files.++## 0.14.4+### Changed+- Target LTS Haskell 13.20 and disable threading in new attempt to fix freezing bug.++## 0.14.3+### Changed+- Remove `-O2`.++## 0.14.2+### Changed+- Target LTS Haskell 12.26 to attempt to fix freezing bug.++## 0.14.1+### Added+- Extra debugging around code that freezes.++### Changed+- Bump dependency on `haskoin-node`.++## 0.14.0+### Removed+- Dump slow protobuf serialization.++### Added+- Add custom serialization.+- Extra debug logging.++### Changed+- Bump `haskoin-core` and `haskoin-node`.++## 0.13.1+### Changed+- Bump `haskoin-node` in `stack.yaml`.+- Do not send empty `getdata` messages.++## 0.13.0+### Added+- Primitive content negotiation for web exceptions.+- Protobuf support for errors.+- Protobuf support for tranasction ids.++### Changed+- Protobuf format changed in non-backwards-compatible manner for transaction ids.++## 0.12.0+### Added+- Support for binary serialization using Protocol Buffers.+- New endpoints for binary raw transactions (not hex-encoded).++### Changed+- Services field now a hex string instead of a number to avoid overflowing signed 64-bit integer.+- Flatten list of block data objects when responding to request for multiple block heights.+- Errors now reported in plain text without container JSON object.+- Transaction broadcasts are responded to with transaction id in plaintext hex (no JSON).+- Remove database snapshots to improve performance.++## 0.11.2+### Changed+- Fix duplicate mempool transaction announcements in event stream.++## 0.11.1+### Removed+- Removed latest block time check.++## 0.11.0+### Changed+- Improve post transactions endpoint.++## 0.10.1+### Changed+- Fix bug where transaction lists from multiple addresses would sort incorrectly.+- Address gap reduced to 20.++## 0.10.0+### Removed+- Remove addresses from transaction lists.+- No longer use container objects for xpub transactions.++## 0.9.3+### Added+- Permissive CORS headers to allow queries from any domain.+- Improved documentation using real-world examples from the BCH testnet.++## 0.9.2+### Added+- HTTP JSON API switch to turn off transaction list when retrieving blocks.++## 0.9.1+### Added+- Total block fees.+- Total block outputs.+- Block subsidy.++### Changed+- Do not consider the blocks less one block away from headers as out of sync in health check.+- Health check now returns HTTP 503 when not OK or out of sync.++## 0.9.0+### Added+- Version to health check output.+- Block weight for segwit.+- Transaction weight for segwit.++## 0.8.1+### Added+- Health check endpoint.++## 0.8.0+### Added+- Limits and skips.+- Add timestamps to transactions.+- Add transaction count to address balance object.+- Add Merkle root to block data.+- Total funds received by an address now shows up in balance.+- Balances for any address that ever had funds show up in xpub endpoints.++### Changed+- Data model update.+- Performance improvement for xpub calls.+- Transactions are returned in reverse mempool/block order (highest or most recent first).+- Balance objects do not get deleted from database ever.++## 0.6.9+### Changed+- Reduce number of coinbase checks to 10,000 ancestors.++## 0.6.8+### Changed+- Further optimize coinbase after height checks.++## 0.6.7+### Changed+- Impose restrictions on recursion for coinbase after height checks.++## 0.6.6+### Added+- Check whether a transaction can be traced back to a coinbase after certain height.++## 0.6.5+### Changed+- Delete transactions in reverse topological order when reverting a block.++## 0.6.4+### Changed+- Do not fail silently when importing orphan transactions into the mempool.++## 0.6.3+### Changed+- Dummy release to bump haskoin-node in stack.yaml.++## 0.6.2+### Changed+- Correct bug where coinbase transactions weren't properly flagged.++## 0.6.1+### Changed+- Compatibility with Bitcoin Cash hard fork.+- Various bug fixes.++## 0.6.0+### Added+- Address balance cache in memory.++### Changed+- Simplify data model further.+- Fix bug importing outputs with UTXO cache.+- Unspent balances cannot be negative.++## 0.5.0+### Added+- Add UTXO cache in memory.+- Get transactions with witness data in segwit networks.++### Changed+- Paths for derivations in xpubs is a list and no longer a string.+- Various bug fixes.++## 0.4.2+### Removed+- Remove extended public key itself from output of relevant endpoints to save bandwidth.++## 0.4.1+### Changed+- Fix bug when deleting coinbase transactions.+- Extended public key API support.++## 0.4.0+### Changed+- Generate events for mempool transactions.+- Respond with entire block data when querying blocks by height.++## 0.3.1+### Changed+- Do not import transactions to mempool while not synced.+- Only sync mempool against a single peer.+- Allow duplicate transactions to fix re-introduced sync bug.++## 0.3.0+### Added+- Update dependencies.+- Keep orphan blocks and deleted transactions in database.+- Add a `mainchain` field for block data and a `deleted` field for transactions.+- Stream records for performance.+- Show witness data for transaction inputs in SegWit network.+- Support RBF in SegWit network.++### Changed+- Refactor all data access code away from actor.+- Refactor import logic away from actor.+- Abstract data access using typeclasses.+- Implement data access using clean layered architecture.+- Make most of import logic code pure.+- Database now in `db` as opposed to `blocks` directory.+- Use latest `haskoin-node`.++### Removed+- Remove some data from peer information output.+- Remove full transaction from address transaction data.+- Remove limits from address transaction data.+- Remove block data from previous output.+- Remove spender from JSON response when output not spent.+- Remove block hash from block reference.++## 0.2.3+### Removed+- Do not send transaction notifications if a block is being imported.++## 0.2.2+### Added+- Peer information endpoint.++### Changed+- Update `haskoin-node`.++## 0.2.1+### Changed+- Fix tests++## 0.2.0+### Added+- Documentation everywhere.+- Ability to retrieve address transactions.++### Changed+- New versions of NQE and Haskoin Node upstream.+- Improve and simplify API.+- Multi-element endpoints return arrays of arrays.+- Database snapshots for all queries are now mandatory.++### Removed+- Retrieving unspent and spent outputs for an address.+- Redundant API endpoints for multiple elements.++## 0.1.3+### Changed+- Fix a bug with transaction notifications.+- Improve handling orphan transactions.++## 0.1.2+### Changed+- Specify dependencies better.++## 0.1.1+### Changed+- Dependency `secp256k1` is now `secp256k1-haskell`.++## 0.1.0+### Added+- New `CHANGELOG.md` file.+- Bitcoin (BTC) and Bitcoin Cash (BCH) compatibility.+- RocksDB database.+- Mempool support.+- HTTP streaming for events.+- CashAddr support.+- Bech32 support.+- Rate limits.++### Changed+- Split out of former `haskoin` repository.+- Use hpack and `package.yaml`.++### Removed+- Removed Stylish Haskell configuration file.+- Remvoed `haskoin-core` and `haskoin-wallet` packages from this repository.
+ README.md view
@@ -0,0 +1,936 @@+# Haskoin Store++[![CI](https://github.com/jprupp/haskoin-store/actions/workflows/ci.yml/badge.svg)](https://github.com/jprupp/haskoin-store/actions/workflows/ci.yml)+[![Hackage haskoin-store](https://img.shields.io/hackage/v/haskoin-store.svg?label=haskoin-store)](https://hackage.haskell.org/package/haskoin-store)+[![Hackage haskoin-store-data](https://img.shields.io/hackage/v/haskoin-store-data.svg?label=haskoin-store-data)](https://hackage.haskell.org/package/haskoin-store-data)+[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jprupp/haskoin-store/blob/master/haskoin-store/LICENSE)++A Bitcoin (BTC) and Bitcoin Cash (BCH) block chain indexer with a REST API,+written in Haskell. It connects to the peer-to-peer network directly, so it+does not need a full node next to it, and keeps its indices in RocksDB.++- [API reference](https://github.com/jprupp/haskoin-store/blob/master/swagger.yaml)+- [Library documentation](https://hackage.haskell.org/package/haskoin-store)+  and [client documentation](https://hackage.haskell.org/package/haskoin-store-data)+- [Changelog](https://github.com/jprupp/haskoin-store/blob/master/CHANGELOG.md)+- [Report a bug](https://github.com/jprupp/haskoin-store/issues)++## Contents++- [Why Haskoin Store](#why-haskoin-store)+- [Design principles](#design-principles)+- [Features](#features)+- [Quick start](#quick-start)+- [Building from source](#building-from-source)+- [Running](#running)+- [Tutorial: following a wallet](#tutorial-following-a-wallet)+- [Configuration](#configuration)+- [API](#api)+- [How it works](#how-it-works)+- [Monitoring](#monitoring)+- [Using it from Haskell](#using-it-from-haskell)+- [Packages](#packages)+- [Development](#development)+- [Related projects](#related-projects)+- [License](#license)++## Why Haskoin Store++A Bitcoin node can tell you what is in block 800000, but not what the balance+of an address is, which transactions belong to a wallet, or which outputs it+can spend. Wallets, block explorers, payment processors and accounting tools+all need exactly those answers, for many users at once, in milliseconds.++The usual way to get them is to run a full node and an indexer next to it, and+operate both. Haskoin Store is one program instead. It connects to the+peer-to-peer network itself, downloads blocks and unconfirmed transactions,+keeps indices by address, transaction and extended public key in RocksDB, and+serves them over a REST API. It has been used to serve public Bitcoin and+Bitcoin Cash APIs such as api.haskoin.com.++## Design principles++- **Index for the questions people ask.** Every index is laid out so that an+  API query is one seek and a short scan in RocksDB, newest first, which keeps+  response times flat as the chain grows.+- **Never half-write.** Each block, and each batch of mempool transactions, is+  one atomic database write. A crash leaves the store at the previous block,+  with no repair step on restart.+- **Unconfirmed transactions matter.** The mempool is indexed and persisted like+  the chain, ordered before confirmed history, and handles replace-by-fee on+  Bitcoin. A wallet sees a payment the moment the network does.+- **Extended public keys are first class.** Wallets are mostly xpubs, so the+  store derives their addresses up to an exact gap, remembers the derivations,+  and can keep large wallets ready in a Redis shared by several stores.+- **Bounded resources.** API limits stop one request from monopolising the+  server, slow event clients are dropped, and RocksDB memory has a budget.+- **Operable.** A health endpoint that tells a load balancer when an instance is+  behind, StatsD metrics for everything, configuration through flags or+  environment variables, and a Docker image.+- **Follow the work, not a node.** The store trusts the chain with the most+  proof of work, as reported by [haskoin-node](https://github.com/jprupp/haskoin-node),+  and checks what it needs to keep its indices consistent. It does not+  re-validate scripts; run it against the network you trust, or next to your+  own node with `--peer`.++## Features++| Area            | What you get                                                                                |+| --------------- | ------------------------------------------------------------------------------------------- |+| Networks        | Bitcoin and Bitcoin Cash: mainnet, testnets and regtest                                     |+| Addresses       | Balances, transactions and unspent outputs (UTXO) of any address, alone or many at once      |+| Xpubs           | Balances, transactions and UTXO of extended public keys, for legacy, nested SegWit and native SegWit addresses, with an optional Redis cache for large wallets |+| Blocks and txs  | Lookup by hash, height or time; raw serialized forms; transactions of a block               |+| Mempool         | Persistent, indexed by arrival time, with replace-by-fee on BTC and pruning of old entries   |+| Formats         | JSON, pretty JSON, or a compact binary encoding                                              |+| Live events     | New blocks and transactions as a JSON stream or over a WebSocket                            |+| Compatibility   | Endpoints in the style of Blockchain.info under `/blockchain/`                               |+| Operations      | Health check, StatsD metrics, RocksDB memory limits, Docker image                           |++### What is not included++- No wallet: it never holds keys or signs anything.+- No full validation of scripts and consensus rules. It follows the chain with+  the most work and indexes what peers send.+- No address or transaction search by anything but its hash; no indices on+  scripts without an address.++## Quick start++With [Docker](https://www.docker.com/):++```sh+git clone https://github.com/jprupp/haskoin-store.git+cd haskoin-store+docker build -t haskoin-store .+docker run -d --name haskoin-store -p 3000:3000 \+  -e NET=btc -e DISCOVER=true \+  -v haskoin-store-data:/home/haskoin/.haskoin-store \+  haskoin-store+curl 'http://localhost:3000/block/best?pretty=true'+```++The first sync downloads and indexes the whole chain, which takes a long+time and a lot of disk. The API answers while it syncs; `/health` reports+whether the node is in sync.++## Building from source++You need [Stack](https://docs.haskellstack.org/) and three C libraries:+[zlib](https://zlib.net/),+[libsecp256k1](https://github.com/bitcoin-core/secp256k1) and+[RocksDB](https://rocksdb.org/). Stack installs the right GHC by itself.++**Debian or Ubuntu**++```sh+sudo apt install git pkg-config zlib1g-dev libsecp256k1-dev librocksdb-dev haskell-stack+```++**Fedora**++```sh+sudo dnf install git pkgconf zlib-devel libsecp256k1-devel rocksdb-devel stack+```++Then build and install the `haskoin-store` binary to `~/.local/bin`:++```sh+git clone https://github.com/jprupp/haskoin-store.git+cd haskoin-store+stack build --copy-bins+~/.local/bin/haskoin-store --help+```++**Any distribution, with [Nix](https://nixos.org/)**: Stack fetches the C+libraries listed in `stack.yaml` from Nix instead of the system.++```sh+stack --nix build --copy-bins+```++## Running++```sh+haskoin-store --net btc --discover+```++Give it either `--discover` or at least one `--peer`; with neither it turns+discovery on by itself. Data goes in a directory named after the network inside+`--dir`, so one data directory can hold several networks.++For a big public deployment, also set `MALLOC_ARENA_MAX=2` (the Docker image+does) to keep glibc from fragmenting RocksDB's memory.++## Tutorial: following a wallet++This walks through what a wallet back end typically does with the API, on a+store that is already running on `localhost:3000`.++### 1. Wait for the store to be ready++```sh+curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/health+```++`200` means the index is caught up with the network. While it syncs you get+`503`, and the body says which check fails.++### 2. Get the balance of a wallet++Most wallets are an extended public key. Ask for its summary, and say which+kind of addresses it derives (`legacy`, the default, `compat` for nested+SegWit, `segwit` for native SegWit):++```sh+curl 'localhost:3000/xpub/xpub6C...?derive=segwit&pretty=true'+```++The answer has confirmed and unconfirmed balances, and the index of the last+used receiving and change address, so the wallet knows where to derive the+next address.++If you get `derive` wrong, the store says so: a wallet with no transactions+of the kind you asked for, but some of another kind, gets a 400 whose message+names the right one, instead of an empty wallet that looks like a zero+balance.++### 3. List its transactions, page by page++```sh+curl 'localhost:3000/xpub/xpub6C.../transactions?derive=segwit&limit=50'+```++Results are newest first, unconfirmed ones on top. To get the next page, start+from the last transaction you received instead of using an offset:++```sh+curl 'localhost:3000/xpub/xpub6C.../transactions?derive=segwit&limit=50&offset=1&height=<last txid>'+```++`offset=1` skips the transaction you already have. Paging by starting point+stays fast however deep you go, and does not shift when new transactions+arrive.++### 4. Find spendable outputs++```sh+curl 'localhost:3000/xpub/xpub6C.../unspent?derive=segwit'+```++Each output comes with its derivation path, so the wallet knows which key signs+it.++### 5. Broadcast a payment++```sh+curl -X POST --data-binary @tx.hex localhost:3000/transactions+```++The body is the signed transaction in hex. The store sends it to its peers and+returns the transaction id. It appears in the mempool, and in the wallet's+history, as soon as a peer relays it back.++### 6. Stay up to date without polling++```sh+curl -N localhost:3000/events+```++Each line is a new block or a mempool change. A wallet back end keeps this open+and refreshes the affected wallets when an event arrives, instead of asking+every few seconds.++After a lost connection, come back with the hash of the last event you+heard, and the stream starts with what came after it:++```sh+curl -N "localhost:3000/events?after=9a5c…"+```++The server keeps its last 10,000 events. It answers 404 when it no longer+has the one you name: read your wallets again, and listen without `after`.++Over a WebSocket at the same path, a client can ask for only the+transactions it cares about. It sends one or more of these, as text:++```json+{"type":"watch","addresses":["bc1q…","1A1z…"]}+{"type":"watch","xpub":"xpub6C…","derive":"segwit"}+{"type":"filterload","filter":"b50f","functions":11,"tweak":0,"flags":0}+```++From then on it hears of a transaction only when it touches one of the+addresses, an address of the key, or matches the Bloom filter (BIP 37, as+SPV wallets use). Blocks are always sent. A key is followed as a wallet+uses it: the gap of unused addresses moves on as payments come. For a key+with several types of address, send one message for each type.++A client that connects and then sends its messages gets every transaction+until the first message is read. To start with a choice, put it in the URL,+of the WebSocket or of the HTTP stream:++```sh+curl -N "localhost:3000/events?quiet=true"                  # blocks only+curl -N "localhost:3000/events?addresses=bc1q…,1A1z…"+curl -N "localhost:3000/events?xpub=xpub6C…&derive=legacy,segwit"+```++`quiet=true` sends blocks alone until the client watches something; a+WebSocket client with a long list connects with it and sends the list in+messages. The [API reference](swagger.yaml) has every message and+parameter, and the binary form of each message.++### 7. Make big wallets fast++Start the store with `--redis` and a Redis (or Valkey) server. Xpubs with at+least `--redis-min-addrs` used addresses, or at least `--redis-min-txs`+transactions in their addresses, are then cached after their first+query and answered from Redis, transactions in full included. Several stores+can share one Redis: they keep each other's cache up to date, and a store that+has not imported a transaction yet still serves it from the cache. Add+`nocache=true` to a query to read the database instead.++Servers behind a load balancer should also be started with `--fleet`+([A fleet of servers](#a-fleet-of-servers)): they then answer every request+alike, and share a cache of xpubs that is built for that.++## Configuration++Every option has an environment variable that sets its default, which is handy+in containers. Booleans in variables accept `true`/`false`, `yes`/`no`,+`on`/`off` and `1`/`0`. `haskoin-store --help` shows the current values.++### Network and peers++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `-n`, `--net` | `NET` | `bch` | Network: `btc`, `bch`, or a test network (see `--help`) |+| `-d`, `--dir` | `DIR` | `~/.haskoin-store` | Data directory |+| `--discover` | `DISCOVER` | off | Discover peers through DNS seeds and relayed addresses; on regtest it finds none, so name a `--peer` |+| `-p`, `--peer` | `PEER` | none | Connect to `HOST[:PORT]`; repeat for more, space-separated in `PEER` |+| `--max-peers` | `MAX_PEERS` | `20` | Most peers to connect to |+| `--peer-timeout` | `PEER_TIMEOUT` | `120` | Seconds before an unresponsive peer is dropped |+| `--max-peer-life` | `MAX_PEER_LIFE` | `172800` | Seconds after which a peer is dropped, so connections rotate |+| `--block-window` | `BLOCK_WINDOW` | `64` | Blocks to request from the syncing peer at a time |++### Mempool++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--no-mempool` | `NO_MEMPOOL` | off | Do not index unconfirmed transactions |+| `--sync-mempool` | `SYNC_MEMPOOL` | off | Ask each peer for its whole mempool once in sync |+| `--wipe-mempool` | `WIPE_MEMPOOL` | off | Empty the mempool area of the database at start |+| `--mempool-timeout` | `MEMPOOL_TIMEOUT` | `14` | Days after which unconfirmed transactions are deleted (0 = never) |++### Web server and limits++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--host` / `--port` | `HOST` / `PORT` | `*` / `3000` | Address the API listens on |+| `--noweb` | `NO_WEB` | off | Index only, no API |+| `--tls-cert` / `--tls-key` | `TLS_CERT` / `TLS_KEY` | none | Serve HTTPS with this certificate chain and private key, both files in PEM |+| `--no-xpub` | `NO_XPUB` | off | Disable the xpub endpoints |+| `--no-blockchain-info` | `NO_BLOCKCHAIN_INFO` | off | Disable the `/blockchain/` endpoints and price lookups |+| `--max-item-count` | `MAX_ITEM_COUNT` | `200000` | Most items in a list (0 = no limit) |+| `--max-full-item-count` | `MAX_FULL_ITEM_COUNT` | `5000` | Most full transactions in a list (0 = no limit) |+| `--max-offset` | `MAX_OFFSET` | `50000` | Largest `offset` (0 = no limit) |+| `--def-item-count` | `DEF_ITEM_COUNT` | `100` | Items in a list when no `limit` is given |+| `--xpub-gap` | `XPUB_GAP` | `32` | Unused addresses kept after the last used one of each chain of an xpub; payments further away are not seen |+| `--xpub-gap-init` | `XPUB_GAP_INIT` | `20` | Addresses looked at for an xpub with no transactions; an xpub with none used among them counts as unused |+| `--ticker-url` | `TICKER_URL` | Blockchain.info ticker | Source of current prices |+| `--price-history-url` | `PRICE_HISTORY_URL` | Blockchain.info price index | Source of historical prices |+| `--ticker-refresh` | `TICKER_REFRESH` | `90000000` | Microseconds between price updates |++### Redis cache++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--redis` | `REDIS` | off | Cache large xpubs in Redis |+| `--redis-url` | `REDIS_URL` | local Redis | Redis URL, such as `redis://host:6379` |+| `--redis-min-addrs` | `REDIS_MIN_ADDRS` | `100` | Cache xpubs with at least this many used addresses |+| `--redis-min-txs` | `REDIS_MIN_TXS` | `1000` | Or with at least this many transactions in their addresses |+| `--redis-max-xpubs` | `REDIS_MAX_XPUBS` | `10000` | Evict the least recently used xpubs past this many |++Every key of the cache starts with `hs2:<network>:`, so a Redis can be shared+by stores of several networks, and by stores of older versions, whose cache+uses other keys. `--redis-max-keys` and `--redis-sync-interval` of older+versions are gone.++### A fleet of servers++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--fleet` | `FLEET` | off | Answer as one of the servers that share the Redis of `--redis` |++Servers behind a load balancer import the same blocks and transactions at+slightly different times, so the same request can get different answers+depending on which of them it reaches. Start every one of them with+`--redis`, the same `--redis-url` and `--fleet`, and they all answer from+the same block and the same mempool:++- **The block** is the newest that every live server has. Each server+  imports blocks when it gets them and keeps its database as it was at each+  of its last six, and a request is answered from the block that the fleet+  agreed on. The fleet moves to a new block in one step, when the last+  server has it.+- **The mempool** is one pool in Redis, which every server offers its+  unconfirmed transactions to and all answer from. A transaction has one+  time of arrival there, so lists come in the same order everywhere.+- **A server that falls behind** answers 503 with `behind-fleet` rather+  than something that the others would not say, and stops counting after+  ten seconds without a report, so that it holds nobody up. A server whose+  health check fails stops reporting.+- **`/events` is the same stream on every server.** What changes the+  shared mempool or moves the fleet to a block writes an event to Redis+  in the same step, and every server sends those events, in that order,+  instead of what its own node saw. A block is told when the whole fleet+  can answer about it. Each server keeps the same last events, so a+  client that reconnects with `after=` may land on any of them.+- **Without Redis** each server answers from its own database, and+  `/health` says 203, as it does for the cache.+- **Large xpubs are cached for the whole fleet**, under the same+  `--redis-min-addrs`, `--redis-min-txs` and `--redis-max-xpubs` as the+  cache of a server alone, which should be the same on every server. An+  entry holds the chain's part of a wallet at the block that the fleet+  answers from, and the pool's part is kept by the pool itself, so a+  cached answer is the one that the database gives, on every server. The+  server that moves the fleet to a new block brings the cache there; in+  the moment before it has, and for a wallet that just gained or lost an+  address, requests are answered from the database. A reorganisation+  empties the cache. With it a request about a wallet of 464 addresses+  and 19,499 transactions takes 1.3 to 2.2 milliseconds, where it took 5+  to 38.++The rules, and the tests that hold Redis to them, are described in "A fleet+that answers as one" in `docs/layered-database.md`.++### Health check++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--max-lagging-blocks` | `MAX_LAGGING_BLOCKS` | `3` | Most blocks the index may lag behind the headers |+| `--max-pending-txs` | `MAX_PENDING_TXS` | `10000` | Most transactions waiting to be imported |+| `--min-peers` | `MIN_PEERS` | `1` | Fewest connected peers |+| `--block-timeout` | `BLOCK_TIMEOUT` | `14400` | Most seconds since the last block |+| `--tx-timeout` | `TX_TIMEOUT` | `1800` | Most seconds since the last transaction |+| `--health-check-interval` | `HEALTH_CHECK_INTERVAL` | `30` | Seconds between checks |++### Metrics and logging++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--statsd` | `STATSD` | off | Send metrics to StatsD |+| `--statsd-host` / `--statsd-port` | `STATSD_HOST` / `STATSD_PORT` | `localhost` / `8125` | StatsD server |+| `--statsd-prefix` | `STATSD_PREFIX` | `haskoin_store` | Prefix of every metric |+| `--statsd-no-events` | `STATSD_NO_EVENTS` | off | Do not send individual samples |+| `--statsd-aggregates` | `STATSD_AGGREGATES` | off | Also send aggregated statistics every second |+| `--debug` | `DEBUG` | off | Verbose logging |++### RocksDB++| Option | Environment | Default | Meaning |+| --- | --- | --- | --- |+| `--no-bloom` | `BLOOM` | on | No Bloom filters on what is read by key; `BLOOM=false` is the same |+| `--max-open-files` | `MAX_OPEN_FILES` | `2000` | Open files; bounds index and filter memory (0 = unlimited) |+| `--write-buffer-mb` | `WRITE_BUFFER_MB` | `256` | Memtables across all column families (0 = unlimited) |+| `--max-wal-mb` | `MAX_WAL_MB` | `1024` | Write-ahead log size that forces flushes (0 = RocksDB default) |+| `--block-cache-mb` | `BLOCK_CACHE_MB` | `1024` | Shared block cache, holding index and filter blocks too (0 = RocksDB default) |++Memory used by RocksDB is roughly the block cache plus the memtables, on top of+the Haskell heap. `/dbstats` shows where it goes.++Bloom filters are on by default. They let RocksDB skip the table files that+cannot have a key, which is most of what a lookup of a transaction, an+output or a balance would otherwise read.++- **What has them:** the data that is read by key: transactions, spenders,+  unspent outputs, blocks, balances and the mempool. The two indices by+  address, which are scanned, do not.+- **What they cost:** about 1.25 bytes for each key, held in the block+  cache. On a chain with billions of keys that is gigabytes: size+  `--block-cache-mb` for it, or turn them off with `--no-bloom`.+- **An existing database** gets filters for the table files that are+  written from now on, so their memory comes gradually, as RocksDB+  compacts. The migration to database version 22 rewrites nearly every+  transaction, and with it most of the filters arrive at once.+- **`--bloom`** was the way to turn them on, and is still accepted. Up to+  version 1.8.1 it built no filters at all on RocksDB 6.24 or later,+  because of a wrong type in the binding, fixed in+  `rocksdb-haskell-jprupp` 2.6.3.++## API++Full reference: [`swagger.yaml`](swagger.yaml), also served at+[api.haskoin.com](https://api.haskoin.com/). It explains the conventions of+the API (amounts, ordering, paging, formats, errors, unconfirmed transactions+and reorganisations) and the reasons for them, as well as every endpoint.++```sh+curl localhost:3000/address/bc1q…/balance+curl localhost:3000/xpub/xpub6C…/balances+curl localhost:3000/transaction/<txid>+curl -N localhost:3000/events              # stream of new blocks and txs+```++- The root of the server, `/`, is the reference of its API as a page that+  can try requests, and `/swagger.yaml` the file behind it.+- Add `?pretty=true` for indented JSON.+- Send `Accept: application/octet-stream` for the binary format: see+  [Binary format](#binary-format) below.+- Lists are paged with `limit` and `offset`, newest first. `height` begins+  a page at a transaction hash, a block hash, a block height or a Unix time+  instead.+- Parameters are checked: a parameter that an endpoint does not accept, or+  a value it cannot read, gives 400 with a message that names it, instead of+  being ignored. The Blockchain.info endpoints accept `format=json`, which+  clients written for them often send.+- Errors come back with an HTTP status, in the format that was asked for:+  400 for bad input, 404 for unknown things, 409 when a short transaction+  number is ambiguous, 413 for requests that ask for too much.++### HTTP and HTTPS++- **`HEAD` and `OPTIONS`** are answered, the second with what a browser+  may send from another site.+- **Answers are compressed** for a client that sends+  `Accept-Encoding: gzip`, but for the stream of events.+- **Long lists go in a body.** The endpoints that take a list of+  addresses, blocks, heights or transactions also take it under `POST`,+  as a form (`application/x-www-form-urlencoded`), so that it does not+  have to fit in a URL. `/transactions` does not: `POST` there publishes+  a transaction.+- **A 503 says when to try again**, in `Retry-After`.+- **HTTPS is optional and off by default.** With `--tls-cert` and+  `--tls-key` the server speaks HTTPS on its port, and nothing else. A+  public service usually has a proxy in front that holds the certificate;+  this is for a server by itself. The files are read at start, so a+  renewed certificate takes a restart.++### Binary format++Every endpoint but those under `/blockchain/` and `/dbstats` also answers+in a compact binary format, which is smaller and faster to decode than+JSON. The Haskell client in `haskoin-store-data` speaks it, and+`swagger.yaml` has a section, "Binary format", that is enough to write a+client in another language: the primitives, the layout of every type, and+what each endpoint returns.++- **Ask for it** with `Accept: application/octet-stream`. In a list of+  types it wins when its `q` weight is above 0 and no less than that of+  `application/json`; a wildcard or no header gives JSON. The answer has+  `Content-Type: application/octet-stream`, and every answer has+  `Vary: Accept`.+- **Errors come in binary too**: one byte that says which error, and a+  message or a list of transactions for those that have one. Up to+  version 1.8.1 they were JSON whatever was asked, so a client that also+  talks to older servers reads the `Content-Type` of an error.+- **Lists start with their length**, the raw lists of blocks and+  transactions included, which up to version 1.8.1 had none.+- **`/events` is the exception**: a stream of events of 33 bytes each,+  with no count, for as long as the connection is open.+- **Over the WebSocket** binary is binary frames, for the events and for+  the commands that choose transactions. The server answers in the kind+  of frame that it was last sent.+- Everything is big-endian, except what is copied from the peer-to-peer+  protocol, such as a whole transaction or block.+++### Endpoints at a glance++| Area | Endpoints |+| --- | --- |+| Blocks | `/block/best`, `/block/latest`, `/block/:hash`, `/blocks`, `/block/height/:h`, `/block/heights`, `/block/time/:t`, `/block/mtp/:t`, each with a `/raw` variant where it makes sense |+| Transactions | `/transaction/:txid`, `/transactions`, `/transactions/block/:hash`, `/transaction/:txid/after/:height`, `POST /transactions`, `/raw` variants |+| Mempool | `/mempool` |+| Addresses | `/address/:addr/{transactions,transactions/full,balance,unspent}` and the multi-address forms `/address/{transactions,transactions/full,balances,unspent}?addresses=…` |+| Xpubs | `/xpub/:xpub`, `/xpub/:xpub/{transactions,transactions/full,balances,unspent}`, `DELETE /xpub/:xpub` (drop from cache); `derive=legacy\|compat\|segwit` picks the address type (`normal`, the old name of `legacy`, still works) |+| Network | `/peers`, `/health` (503 when unhealthy, 203 without its Redis), `/dbstats` |+| Events | `/events`, as JSON lines over HTTP or as a WebSocket; `after=` resumes, and a WebSocket client can filter by address, xpub or Bloom filter |+| Blockchain.info | `/blockchain/multiaddr`, `/blockchain/balance`, `/blockchain/rawaddr/:addr`, `/blockchain/unspent`, `/blockchain/rawtx/:txid`, `/blockchain/rawblock/:hash`, `/blockchain/latestblock`, `/blockchain/unconfirmed-transactions`, `/blockchain/export-history`, `/blockchain/q/…` |++Turn off the xpub endpoints with `--no-xpub` and the Blockchain.info ones+with `--no-blockchain-info`.++## How it works++```+peers ──► haskoin-node ──► block store ──► RocksDB ──► web server ──► clients+                │               │             ▲+                └─ headers      └─ events ──► cache ◄──► Redis ◄──► other stores+```++- **haskoin-node** keeps a pool of peers and syncs block headers.+- The **block store** (`Haskoin.Store.BlockStore`) downloads blocks from one+  peer at a time and mempool transactions from all of them, and imports them+  with the rules in `Haskoin.Store.Logic.Layered`. Each import is one atomic+  RocksDB batch, so a crash never leaves a half-written block.+- Reorganisations are handled by reverting blocks until the best block is+  back on the main chain of headers.+- The **cache** (`Haskoin.Store.Cache`) keeps xpubs with many used addresses+  or many transactions in Redis: their balances, transactions, unspent+  outputs and the bodies of their transactions. It never computes an entry+  from what it guesses changed: when a block, a mempool transaction or a+  reorganisation touches a cached xpub, the store reads from RocksDB, with+  the same queries that answer uncached requests, the transactions that+  changed and the outputs they create and spend, and writes the differences+  in one Lua script, together with every transaction body the entry needs+  that Redis does not have yet (bodies it has, such as those of the+  transactions a block confirms, are rewritten just before, in small+  scripts, so that a large block does not hold Redis for long).+  The time of a reload no longer grows with the size of the xpub. The store+  reads the whole xpub instead when it first caches it, and whenever it+  cannot be sure the entry has every earlier change: when the entry is more+  than a block behind, when an address with transactions enters or leaves+  the gap, when the balance of an address that no change touched differs+  from Redis's and its newest transactions do not explain why, and on a+  schedule, every five to ten minutes. The interval depends on the xpub, so+  that xpubs cached together are not all read whole at the same moment,+  which would hold them all in memory at once. Under load the database is+  often a little ahead of the events being applied, so a reload first adds+  the transactions that the database has for such an address since the+  block the entry was written at, and the mempool transactions of the entry+  that have since been confirmed or dropped. The script+  writes nothing if the entry changed since it was read, so the entry is+  always what the database says; and Redis, which answers one client at a+  time, is held for the size of the change, not of the xpub. A page of a+  cached xpub is one round trip to Redis. The Lua scripts are in+  `src/Haskoin/Store/Cache/lua/`, one file each, embedded in the library+  when it is compiled. Stores sharing a Redis claim each change, so it+  is applied once, by the first store to see it; a store never replaces an+  entry that knows more than its database (a higher block, or a transaction+  it has not seen), and a store that is catching up does no cache work.+  Without Redis, xpub queries read RocksDB directly. The ways an entry can+  still come to differ from the database unnoticed, such as a missed spend of+  an output of a cached transaction, and what would rule them out, are in+  `docs/cache-consistency.md`.+- A **fleet** (`Haskoin.Store.Fleet.Member`, with `--fleet`) has a cache of+  xpubs of another kind (`Haskoin.Store.Fleet.XPub`), because its servers+  do not answer from their own databases but from one block and one shared+  mempool. An entry has two layers: what the chain has for the addresses of+  the xpub at the block that the fleet answers from, and what the shared+  pool has for them, which the script of the pool writes as it changes the+  pool. The first follows from the chain, so any server writes the same,+  and nothing has to be claimed or repaired: the server that moves the+  fleet to a new block applies what the block did, and a cache that was+  not brought along is emptied. `docs/layered-database.md` has the design,+  the tests and the measurements.+- The **web server** (`Haskoin.Store.Web`) answers from RocksDB, and from the+  cache for cached xpubs and for transaction bodies that Redis has.++Xpub addresses are derived up to an exact gap: every address up to the last+used one of each chain, then exactly `--xpub-gap` unused ones, and always the+first `--xpub-gap-init` addresses. A payment further away is not seen, as a+wallet following BIP 44 with the same gap would not see it; raise the gap to+see it.++An xpub does not record which kind of address its wallet derives, so a+request names it with `derive`. When an xpub has no transactions with the+kind asked for, the store reads the first `--xpub-gap-init` addresses of the+other kinds, and if one of them is used it refuses the request with 400 and+names that kind: an empty answer would look like a wallet with no money, a+mistake that is easy to miss. The extra reads happen only for wallets that+look empty, and not at all on networks without SegWit, which have one kind of+address.++The database layout is described in `Haskoin.Store.Database.Types`.++Unconfirmed transactions are kept in an area of their own, and applied on+top of the chain when a query is answered, so that nothing the mempool does+changes the chain's data. This is version 21 of the database, the layered+layout of `docs/layered-database.md`, which has the reasons, the+measurements and the evidence. Up to version 20 they were written into the+same indices as the block chain. A database of version 20 is migrated the+first time this version of the store opens it: its mempool is moved to the+new area, in batches that can be interrupted, and the log says how many+transactions were moved. **There is no way back**: an older store refuses+the database afterwards, so take a copy of a production database first.++Version 22 makes the record of a confirmed transaction something that is+written once, by the block that confirms it, and the same on every server+that has that block.++- **Its time is the time of its block.** Until then a server that had seen+  a transaction in its mempool kept the time of that when a block+  confirmed it, so two servers gave two times for one confirmed+  transaction, and a server that synced later gave a third.+- **The spenders of its outputs are under keys of their own**, sixteen+  outputs to a key. Until then they were in the record, which was read and+  written whole again each time a block spent one of its outputs. Reading+  a confirmed transaction in full now takes a second key: 18 microseconds+  where it took 11.5, measured, and 0.8 milliseconds for a hundred where+  it took 0.4. Lists by address and by xpub do not read records and cost+  the same.++A transaction that leaves the mempool unconfirmed, because it was replaced,+double-spent by a block or too old, is no longer kept: the store forgets+it, and the API answers 404 for it, as it already did. Until version 22+its body stayed in the database, marked as deleted, for ever. The bodies+that are there in a migrated database are left where they are and+ignored, since finding them would mean reading every transaction.++A database of version 21, or of version 20 once its mempool is moved, is+brought to this the first time this version of the store opens it.+**The store reads every transaction that it has, and writes nearly every+one again, before it starts**: nothing lists the records that need it, and+most have a spender in them. The log says how far it is every million+transactions. A database of Bitcoin has more than a billion, so plan for+many hours, and for room on the disk for a second copy of the transactions+until RocksDB has compacted the first away. It can be interrupted, and+starts again from the first transaction, skipping what is done. As before,+**there is no way back**: take a copy first.+The parts:++- the migration from version 20 (`Haskoin.Store.Database.Migrate`);+- the keys that a mempool transaction writes, the summary that is kept for+  each address they touch, and what the mempool does to the balance and the+  unspent outputs of an address (`Haskoin.Store.Database.Mempool`);+- the rules that import blocks and transactions in that layout+  (`Haskoin.Store.Logic.Layered`), which the tests hold to the results of+  the rules of version 20, operation by operation and key by key;+- the queries over both areas (`Haskoin.Store.Database.Query.Layered`), each+  on a snapshot of the database, one for each request of the web server,+  which the tests hold to the answers of the queries of version 20, page by+  page.++The rules and queries of version 20 (`Haskoin.Store.Logic`,+`Haskoin.Store.Database.Query`) stay in the library for those tests, and+nothing in the store calls them.++## Monitoring++- **`/health`** returns the result of the latest check as JSON, with status 200+  when every check passes and 503 otherwise. The checks are: index lag behind+  the headers, age of the last block and of the last transaction, transactions+  waiting to be imported, and connected peers. Point your load balancer at it.+- **Without Redis** a store that has a cache goes on answering, from its own+  database, and `/health` says 203 instead of 200, with the header+  `Haskoin-Degraded: redis` and `"redis": {"ok": false}` in its JSON. A request waits at most two seconds for Redis+  before the store gives up on it, and the store tries again at every health+  check. 203 is in the range that load balancers take for healthy, so the+  store stays in rotation; alert on anything but 200 to hear of it.+- **StatsD**: with `--statsd`, the store reports the timing of every kind of+  request (`web.*`), database reads (`db.*`), cache hits, misses, reloads and+  full reloads (`cache.*`),+  and block store gauges such as block and header heights and mempool size+  (`store.*`). A server of a fleet reports what it does with the fleet's+  cache of xpubs under `fleet.cache.*`:++  | Metric | Kind | Meaning |+  | --- | --- | --- |+  | `fleet.cache.hits` | counter | Requests about an xpub whose balances the cache gave |+  | `fleet.cache.misses` | counter | Requests about an xpub that the database answered |+  | `fleet.cache.builds` | counter | Entries that this server built, or added addresses to |+  | `fleet.cache.build` | timing | Milliseconds to build an entry |+  | `fleet.cache.abandoned` | counter | Builds given up because the fleet or the entry moved on |+  | `fleet.cache.evictions` | counter | Entries deleted for being the least recently used |+  | `fleet.cache.emptied` | counter | Times that this server emptied the cache |+  | `fleet.cache.xpubs` | gauge | Xpubs in the cache, whichever server cached them |+  | `fleet.cache.addresses` | gauge | Addresses that those xpubs have between them |++  Misses include small xpubs, which are never cached, so the share of hits+  says how much of the load the cache takes, not how well it holds what it+  has. `fleet.cache.emptied` should move only at a reorganisation or when a+  server stops at the wrong moment: the log of the server says why each+  time, at the level of information. With `--debug` it also has a line for+  each entry built and each block that the cache was brought to.+- **`/dbstats`** returns RocksDB memory use per column family and RocksDB's own+  statistics, as text.+- **In a fleet** (`--fleet`), any request can be answered 503 with+  `{"error": "behind-fleet"}` by a server that does not have the block that+  the fleet answers from. Have the load balancer try another server, and+  alert if one keeps saying it: its `/health` will say why.+- **Memory.** The resident memory of the store is its GHC heap plus RocksDB:+  the block cache and memtables fill up to `--block-cache-mb` and+  `--write-buffer-mb`, then stay there. The executable is built with+  `--disable-delayed-os-memory-return`, so memory that the heap no longer+  needs goes back to the system at once. Without it, Linux keeps counting+  returned memory as resident (`LazyFree` in `/proc/<pid>/smaps`) until it+  needs it, and the heap looks as if it never shrinks after a peak, such as+  the full reload of a large cached xpub. Pass `+RTS+  --disable-delayed-os-memory-return -RTS` yourself if you build the store+  some other way. To tell such memory from a leak, see+  `stress/results/2026-11-09-memory/README.md`: how to read `smaps`, and a+  heap profile of a busy store, with live data flat at about 10 MiB.+- **Logs** go to standard error, one line per message, tagged with the+  component, such as `[Info#BlockStore] Best block: ...`.++## Using it from Haskell++Query a running server with `haskoin-store-data`:++```haskell+import Control.Monad.Except (runExceptT)+import Data.Default (def)+import Haskoin (btc, textToAddr, withContext)+import Haskoin.Store.WebClient++main :: IO ()+main = withContext $ \ctx -> do+  let cfg = def {net = btc} -- https://api.haskoin.com/btc/...+      Just addr = textToAddr btc "bc1q…"+  r <- runExceptT $ apiCall ctx cfg (GetAddrBalance addr)+  print r+```++The client appends the network name to `host`, as in the layout of+api.haskoin.com. To reach your own server, put it behind a proxy that maps+`/btc/` (or the name of your network) to it.++Or embed the indexer in your own program with `haskoin-store`: `withStore`+starts the node, the block store and the cache from a `StoreConfig`, and the+functions of `Haskoin.Store.Database.Query.Layered` query its database in+`IO`, on a view of it taken with `withView`, such as+`withView store.db (\v -> getBalance v addr)`.+Subscribe to `Store.pub` to receive `StoreEvent`s.++Every module has Haddock documentation; build it with `stack haddock`.++## Packages++Haskoin Store is two Haskell packages, each in its own repository:++- **`haskoin-store`**, this repository: the indexer, the web server and the+  executable.+- **[`haskoin-store-data`](https://github.com/jprupp/haskoin-store-data)**:+  the data types, their JSON and binary encodings, and a client for the API+  (`Haskoin.Store.WebClient`). Use this one to talk to a Haskoin Store server+  from Haskell. It is small to depend on, as it has no database or node.++They were released together from this repository up to 1.8.1. A change to+the wire format is made in haskoin-store-data, released, and then picked up+here; `swagger.yaml` stays here and describes the server.++## Development++```sh+stack build --test --no-run-tests+stack test                     # includes a 6000-block heap-growth test+scripts/format                 # ormolu, the code style used here+scripts/bench-web.sh           # load-test the API with wrk+scripts/stress-cache.sh        # stress the Redis cache, with a report+scripts/check-local-deps.sh    # what CI checks: no local paths in stack.yaml+scripts/install-system-libraries.sh  # the C libraries, on Ubuntu or Debian+```++`stack.yaml` must pin released versions of every dependency. Pointing+`extra-deps` at a sibling checkout such as `../haskoin-core` is fine for+local testing, but CI rejects it.++The tests use [hspec](https://hspec.github.io/). The unit tests need no network+access. The integration tests (`Haskoin.Store.IntegrationSpec`) run regtest+chains of Bitcoin Core and Bitcoin Cash Node, and Valkey, in+[Podman](https://podman.io/) containers, and check the store against them: the+exact gap, spends and change, mempool order, multiaddr counts, reorganisations,+and that the cache answers exactly like the database on every xpub endpoint.++- They are skipped if Podman cannot run containers, or if+  `HASKOIN_STORE_NO_PODMAN` is set.+- The first run builds the image `localhost/haskoin-store-test`, which needs+  network access: Ubuntu 26.04 pinned by digest, Valkey from a fixed snapshot of+  the Ubuntu archive, and the release tarballs of both nodes, checked against+  SHA-256 hashes pinned in `test/Haskoin/Store/Integration/Image.hs`. No+  third-party image is used.+- Containers are removed when the tests end, data lives in tmpfs mounts and+  under `/dev/shm`, and every container has a memory limit.+- Set `HASKOIN_TEST_LOG=1` to print the log of the store under test.++The test suite runs with at most four cores and a 4 GiB heap, so that a+runaway test fails instead of exhausting the machine.++`package.yaml` is the source of the package description. Change it rather+than `haskoin-store.cabal`, which+[hpack](https://github.com/sol/hpack) generates.++### Stress test of the cache++`scripts/stress-cache.sh [OUT_DIR] [-- OPTIONS]` measures the cache while it+works hard. It runs a regtest chain of Bitcoin Core, Valkey and wrk in Podman+containers from the test image, and the `haskoin-store` binary with the+cache on. A few wallets, whose keys the stress program (`stress/Main.hs`)+holds, make hundreds of thousands of transactions between them, signed by+the program and sent to the node in batches, with a block after each batch.+Every transaction changes two cached xpubs, so the cache reloads them over+and over, while wrk asks the server about the xpubs on five endpoints+(summary, balances, transactions, unspent outputs and multiaddr) the whole+time. At the end it compares the cached answers with those of the database,+and `scripts/stress-report.py` writes `OUT_DIR/report.md`: how fast the+transactions went in, how many reloads the cache did, how many of them read+the whole xpub, and how long they took as the xpubs grew, how far the server+fell behind, its CPU and memory, the+memory of Valkey, the commands that blocked Redis for 10 ms or more (from+its slow log, by script, with the number of arguments of each, which grows+with the size of a commit), and the latencies of each endpoint over time.++- The defaults are 200,000 transactions between 4 wallets of 200 receiving+  and 200 change addresses, 2,000 per block; pass `-- --help` for the+  options, and `-- --txs 20000` for a run of a few minutes. A default run+  takes about half an hour and up to about 7 GiB of memory.+- The raw measurements stay in `OUT_DIR` as JSON lines (`rounds.jsonl`,+  `samples.jsonl`, `wrk.jsonl`), with `summary.json` and the log of the+  server.+- No container is left behind, even if the script is interrupted or+  killed: the program removes its containers when it ends, and the script+  removes any that carry its label and stops the server.+- Results of past runs are kept in `stress/results/<date>/` for comparison:+  the report, the measurements (without the server log), and a README with+  the commits, the machine and what the run found. A new run worth keeping+  goes there too.+- To look for a leak, run the server with a heap profile by type, which needs+  no profiling build, and summarise it: the live heap over time and the types+  of closure that grew most.++  ```sh+  scripts/stress-cache.sh /tmp/run -- --txs 60000 \+    --server-rts '-N4 -M2g -T -hT -i10 -po/tmp/heap'+  scripts/heap-profile-summary.py /tmp/heap.hp+  ```++### Measurements for the layered database++`docs/layered-database/` has the programs behind the numbers in+`docs/layered-database.md`, with their output. They are not part of the+package; each says in its header how to build and run it.++- `MigrateBench.hs` times the migration to the layered layout against+  wiping the same mempool.+- `QueryCost.hs` writes the same synthetic history into a database of each+  layout, with each set of rules, and times both: importing blocks and+  mempool transactions, and every kind of query, for addresses and wallets+  that have nothing, a little and much in the mempool. It checks that both+  databases give the same answers.+- `FleetCost.hs` asks one store the same questions alone and as a member+  of a fleet, with its mempool in a pool in Redis, checks that the answers+  are the same, and times both. It asks about wallets a third way too,+  through the fleet's cache of xpubs.++### Contributing++Pull requests are welcome.++- Format the code with `scripts/format` (Ormolu), and keep every line of+  code shorter than 120 columns.+- Put scripts in files of their own rather than embedding them: Lua for+  Redis under `src/Haskoin/Store/Cache/lua/`, shell under `scripts/`, called+  from CI rather than written into the workflow.+- Add tests for the behaviour you change.+- Describe the change under a new version in `CHANGELOG.md`, which follows+  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).+- Update `swagger.yaml` when you change the API.++## Related projects++- [haskoin-core](https://github.com/jprupp/haskoin-core) has the Bitcoin data+  types and protocol.+- [haskoin-node](https://github.com/jprupp/haskoin-node) connects to peers and+  syncs headers.+- [nqe](https://github.com/jprupp/nqe),+  [rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell),+  [rocksdb-query](https://github.com/jprupp/rocksdb-query) and+  [statsd-rupp](https://github.com/jprupp/statsd-rupp) provide concurrency,+  storage and metrics.++## License++Haskoin Store is released under the+[MIT License](https://github.com/jprupp/haskoin-store/blob/master/haskoin-store/LICENSE).
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
app/Main.hs view
@@ -1,20 +1,10 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE CPP #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}  module Main (main) where  import Control.Monad (when)-import Control.Monad.Cont (ContT (ContT), runContT)-import Control.Monad.Trans (lift) import Data.Char (toLower) import Data.Default (Default (..)) import Data.List (intercalate)@@ -34,7 +24,6 @@     withLogger,     withSource,   )-import Haskoin.Store.Common (loopForever) import Haskoin.Store   ( Store (..),     StoreConfig (..),@@ -43,9 +32,11 @@     runWeb,     withStore,   )+import Haskoin.Store.Common (loopForever) import NQE (receive, withSubscription) import Options.Applicative-  ( Parser,+  ( Mod,+    Parser,     auto,     execParser,     flag,@@ -53,6 +44,7 @@     header,     help,     helper,+    hidden,     info,     long,     many,@@ -79,7 +71,6 @@     withStats,   ) import Text.Read (readMaybe)-import UnliftIO (MonadIO) import UnliftIO.Directory   ( createDirectoryIfMissing,     getAppUserDataDirectory,@@ -112,10 +103,11 @@     minPeers :: !Int,     webLimits :: !WebLimits,     redis :: !Bool,+    fleet :: !Bool,     redisURL :: !String,     redisMinAddrs :: !Int,-    redisMaxKeys :: !Integer,-    redisSyncInterval :: !Int,+    redisMinTxs :: !Int,+    redisMaxXPubs :: !Int,     noMempool :: !Bool,     wipeMempool :: !Bool,     syncMempool :: !Bool,@@ -136,6 +128,8 @@     noBlockchainInfo :: !Bool,     noXPub :: !Bool,     healthCheckInterval :: !Int,+    tlsCert :: !String,+    tlsKey :: !String,     bloom :: !Bool,     noWeb :: !Bool,     maxOpenFiles :: !Int,@@ -145,13 +139,13 @@   }  -- | Read an environment variable with a parser, or return the default if it is unset or invalid.-env :: (MonadIO m) => String -> a -> (String -> Maybe a) -> m a+env :: String -> a -> (String -> Maybe a) -> IO a env e d p = do   ms <- lookupEnv e   return $ fromMaybe d $ p =<< ms  -- | Default configuration, taken from environment variables where set.-defConfig :: (MonadIO m) => m Config+defConfig :: IO Config defConfig = do   dir <-     getDir@@ -177,14 +171,16 @@     getWebLimits   redis <-     env "REDIS" False parseBool+  fleet <-+    env "FLEET" False parseBool   redisURL <-     env "REDIS_URL" "" pure   redisMinAddrs <-     env "REDIS_MIN_ADDRS" 100 readMaybe-  redisMaxKeys <--    env "REDIS_MAX_KEYS" 100000000 readMaybe-  redisSyncInterval <--    env "REDIS_SYNC_INTERVAL" 30 readMaybe+  redisMinTxs <-+    env "REDIS_MIN_TXS" 1000 readMaybe+  redisMaxXPubs <-+    env "REDIS_MAX_XPUBS" 10000 readMaybe   noMempool <-     env "NO_MEMPOOL" False parseBool   wipeMempool <-@@ -226,9 +222,13 @@   healthCheckInterval <-     env "HEALTH_CHECK_INTERVAL" 30 readMaybe   bloom <--    env "BLOOM" False parseBool+    env "BLOOM" True parseBool   noWeb <-     env "NO_WEB" False parseBool+  tlsCert <-+    env "TLS_CERT" "" pure+  tlsKey <-+    env "TLS_KEY" "" pure   maxOpenFiles <-     env "MAX_OPEN_FILES" 2000 readMaybe   writeBufferMB <-@@ -283,6 +283,12 @@   "0" -> Just False   _ -> Nothing +-- | The help of an option, with the environment variable that sets its+-- default: every option has one, and someone who runs the store in a+-- container reads the help to find its name.+helpEnv :: String -> String -> Mod f a+helpEnv name text = help (text <> " [env: " <> name <> "]")+ -- | Command line parser, with defaults from the given configuration. config :: Config -> Parser Config config c = do@@ -291,21 +297,21 @@       metavar "DIRECTORY"         <> long "dir"         <> short 'd'-        <> help "Data directory"+        <> helpEnv "DIR" "Data directory"         <> showDefault         <> value c.dir   host <-     strOption $       metavar "ADDRESS"         <> long "host"-        <> help "Network address to bind"+        <> helpEnv "HOST" "Network address to bind"         <> showDefault         <> value c.host   port <-     option auto $       metavar "PORT"         <> long "port"-        <> help "REST API listening port"+        <> helpEnv "PORT" "REST API listening port"         <> showDefault         <> value c.port   net <-@@ -313,21 +319,21 @@       metavar netNames         <> long "net"         <> short 'n'-        <> help "Network to connect to"+        <> helpEnv "NET" "Network to connect to"         <> showDefault         <> value c.net   discover <-     flag c.discover True $       long "discover"-        <> help "Peer discovery"+        <> helpEnv "DISCOVER" "Peer discovery"   peers <-     fmap (mappend c.peers) $       many $-        option auto $+        strOption $           metavar "HOSTNAME[:PORT]"             <> long "peer"             <> short 'p'-            <> help "Network peer (as many as required)"+            <> helpEnv "PEER" "Network peer (as many as required)"   version <-     switch $       long "version"@@ -336,270 +342,308 @@   debug <-     flag c.debug True $       long "debug"-        <> help "Show debug messages"+        <> helpEnv "DEBUG" "Show debug messages"   maxPendingTxs <-     option auto $       metavar "COUNT"         <> long "max-pending-txs"-        <> help "Maximum pending txs to fail health check"+        <> helpEnv "MAX_PENDING_TXS" "Maximum pending txs to fail health check"         <> showDefault         <> value c.maxPendingTxs   maxLaggingBlocks <-     option auto $       metavar "COUNT"         <> long "max-lagging-blocks"-        <> help "Maximum number of unindexed blocks"+        <> helpEnv "MAX_LAGGING_BLOCKS" "Maximum number of unindexed blocks"         <> showDefault         <> value c.maxLaggingBlocks   minPeers <-     option auto $       metavar "COUNT"         <> long "min-peers"-        <> help "Minimum number of connected peers for health check"+        <> helpEnv "MIN_PEERS" "Minimum number of connected peers for health check"         <> showDefault         <> value c.minPeers   noWeb <-     flag c.noWeb True $       long "noweb"-        <> help "Disable web server"+        <> helpEnv "NO_WEB" "Disable web server"   webLimits <- do     maxItemCount <-       option auto $         metavar "COUNT"           <> long "max-item-count"-          <> help "Hard limit for simple listings (0 = inf)"+          <> helpEnv "MAX_ITEM_COUNT" "Hard limit for simple listings (0 = inf)"           <> showDefault           <> value c.webLimits.maxItemCount     maxFullItemCount <-       option auto $         metavar "COUNT"           <> long "max-full-item-count"-          <> help "Hard limit for full listings (0 = inf)"+          <> helpEnv "MAX_FULL_ITEM_COUNT" "Hard limit for full listings (0 = inf)"           <> showDefault           <> value c.webLimits.maxFullItemCount     maxOffset <-       option auto $         metavar "OFFSET"           <> long "max-offset"-          <> help "Hard limit for offsets (0 = inf)"+          <> helpEnv "MAX_OFFSET" "Hard limit for offsets (0 = inf)"           <> showDefault           <> value c.webLimits.maxOffset     defItemCount <-       option auto $         metavar "COUNT"           <> long "def-item-count"-          <> help "Soft default limit (0 = inf)"+          <> helpEnv "DEF_ITEM_COUNT" "Soft default limit (0 = inf)"           <> showDefault           <> value c.webLimits.defItemCount     xpubGap <-       option auto $         metavar "GAP"           <> long "xpub-gap"-          <> help "Max gap for xpub queries"+          <> helpEnv "XPUB_GAP" "Max gap for xpub queries"           <> showDefault           <> value c.webLimits.xpubGap     xpubGapInit <-       option auto $         metavar "GAP"           <> long "xpub-gap-init"-          <> help "Max gap for empty xpubs"+          <> helpEnv "XPUB_GAP_INIT" "Max gap for empty xpubs"           <> showDefault           <> value c.webLimits.xpubGapInit     blockTimeout <-       option auto $         metavar "SECONDS"           <> long "block-timeout"-          <> help "Block lag health timeout"+          <> helpEnv "BLOCK_TIMEOUT" "Block lag health timeout"           <> showDefault           <> value c.webLimits.blockTimeout     txTimeout <-       option auto $         metavar "SECONDS"           <> long "tx-timeout"-          <> help "Last tx received health timeout"+          <> helpEnv "TX_TIMEOUT" "Last tx received health timeout"           <> showDefault           <> value c.webLimits.txTimeout     return WebLimits {..}   redis <-     flag c.redis True $       long "redis"-        <> help "Redis cache for xpub data"+        <> helpEnv "REDIS" "Redis cache for xpub data"+  fleet <-+    flag c.fleet True $+      long "fleet"+        <> helpEnv+          "FLEET"+          ( unwords+              [ "Answer as one of the servers that share the Redis of --redis:",+                "all from the same block and the same mempool"+              ]+          )   redisURL <-     strOption $       metavar "URL"         <> long "redis-url"-        <> help "URL for Redis cache"+        <> helpEnv "REDIS_URL" "URL for Redis cache"         <> value c.redisURL   redisMinAddrs <-     option auto $-      metavar "GAP"+      metavar "COUNT"         <> long "redis-min-addrs"-        <> help "Minimum xpub address count to cache in Redis"+        <> helpEnv "REDIS_MIN_ADDRS" "Cache xpubs with at least this many used addresses"         <> showDefault         <> value c.redisMinAddrs-  redisMaxKeys <-+  redisMinTxs <-     option auto $       metavar "COUNT"-        <> long "redis-max-keys"-        <> help "Maximum number of keys in Redis"+        <> long "redis-min-txs"+        <> helpEnv "REDIS_MIN_TXS" "Cache xpubs with at least this many transactions in their addresses"         <> showDefault-        <> value c.redisMaxKeys-  redisSyncInterval <-+        <> value c.redisMinTxs+  redisMaxXPubs <-     option auto $-      metavar "SECONDS"-        <> long "redis-sync-interval"-        <> help "Sync mempool to Redis interval"+      metavar "COUNT"+        <> long "redis-max-xpubs"+        <> helpEnv "REDIS_MAX_XPUBS" "Evict the least recently used xpubs from Redis past this many"         <> showDefault-        <> value c.redisSyncInterval+        <> value c.redisMaxXPubs   noMempool <-     flag c.noMempool True $       long "no-mempool"-        <> help "Do not index mempool transactions"+        <> helpEnv "NO_MEMPOOL" "Do not index mempool transactions"   wipeMempool <-     flag c.wipeMempool True $       long "wipe-mempool"-        <> help "Wipe indexed mempool at start"+        <> helpEnv "WIPE_MEMPOOL" "Wipe indexed mempool at start"   syncMempool <-     flag c.syncMempool True $       long "sync-mempool"-        <> help "Attempt to download peer mempools"+        <> helpEnv "SYNC_MEMPOOL" "Attempt to download peer mempools"   blockWindow <-     option auto $       metavar "COUNT"         <> long "block-window"-        <> help "Blocks to request from the syncing peer at a time"+        <> helpEnv "BLOCK_WINDOW" "Blocks to request from the syncing peer at a time"         <> showDefault         <> value c.blockWindow   mempoolTimeout <-     option auto $       metavar "DAYS"         <> long "mempool-timeout"-        <> help "Cull old mempool transactions"+        <> helpEnv "MEMPOOL_TIMEOUT" "Cull old mempool transactions"         <> showDefault         <> value c.mempoolTimeout   peerTimeout <-     option auto $       metavar "SECONDS"         <> long "peer-timeout"-        <> help "Unresponsive peer timeout"+        <> helpEnv "PEER_TIMEOUT" "Unresponsive peer timeout"         <> showDefault         <> value c.peerTimeout   maxPeerLife <-     option auto $       metavar "SECONDS"         <> long "max-peer-life"-        <> help "Maximum peer connection time"+        <> helpEnv "MAX_PEER_LIFE" "Maximum peer connection time"         <> showDefault         <> value c.maxPeerLife   maxPeers <-     option auto $       metavar "COUNT"         <> long "max-peers"-        <> help "Do not connect to more than this many peers"+        <> helpEnv "MAX_PEERS" "Do not connect to more than this many peers"         <> showDefault         <> value c.maxPeers   statsd <-     flag c.statsd True $       long "statsd"-        <> help "Enable statsd metrics"+        <> helpEnv "STATSD" "Enable statsd metrics"   statsdHost <-     strOption $       metavar "HOSTNAME"         <> long "statsd-host"-        <> help "Host to send statsd metrics"+        <> helpEnv "STATSD_HOST" "Host to send statsd metrics"         <> showDefault         <> value c.statsdHost   statsdPort <-     option auto $       metavar "PORT"         <> long "statsd-port"-        <> help "Port to send statsd metrics"+        <> helpEnv "STATSD_PORT" "Port to send statsd metrics"         <> showDefault         <> value c.statsdPort   statsdPrefix <-     strOption $       metavar "PREFIX"         <> long "statsd-prefix"-        <> help "Prefix for statsd metrics"+        <> helpEnv "STATSD_PREFIX" "Prefix for statsd metrics"         <> showDefault         <> value c.statsdPrefix   statsdNoEvents <-     flag c.statsdNoEvents True $       long "statsd-no-events"-        <> help "Do not report individual events to statsd"+        <> helpEnv "STATSD_NO_EVENTS" "Do not report individual events to statsd"   statsdAggregates <-     flag c.statsdAggregates True $       long "statsd-aggregates"-        <> help "Send statistical aggregate reports to statsd every second"+        <> helpEnv "STATSD_AGGREGATES" "Send statistical aggregate reports to statsd every second"   tickerRefresh <-     option auto $       metavar "MICROSECONDS"         <> long "ticker-refresh"-        <> help "How often to retrieve price information"+        <> helpEnv "TICKER_REFRESH" "How often to retrieve price information"         <> showDefault         <> value c.tickerRefresh   tickerURL <-     strOption $       metavar "URL"         <> long "ticker-url"-        <> help "Blockchain.info price ticker URL"+        <> helpEnv "TICKER_URL" "Blockchain.info price ticker URL"         <> showDefault         <> value c.tickerURL   priceHistoryURL <-     strOption $       metavar "URL"         <> long "price-history-url"-        <> help "Blockchain.info price history URL"+        <> helpEnv "PRICE_HISTORY_URL" "Blockchain.info price history URL"         <> showDefault         <> value c.priceHistoryURL   noBlockchainInfo <-     flag c.noBlockchainInfo True $       long "no-blockchain-info"-        <> help "Disable Blockchain.info-style API endpoints"+        <> helpEnv "NO_BLOCKCHAIN_INFO" "Disable Blockchain.info-style API endpoints"   noXPub <-     flag c.noXPub True $       long "no-xpub"-        <> help "Disable extended public key support"+        <> helpEnv "NO_XPUB" "Disable extended public key support"   healthCheckInterval <-     option auto $       metavar "SECONDS"         <> long "health-check-interval"-        <> help "Background check update interval"+        <> helpEnv "HEALTH_CHECK_INTERVAL" "Background check update interval"         <> showDefault         <> value c.healthCheckInterval-  bloom <--    flag c.bloom True $-      long "bloom"-        <> help "RocksDB Bloom filters"+  tlsCert <-+    strOption $+      metavar "FILE"+        <> long "tls-cert"+        <> helpEnv "TLS_CERT" "Serve HTTPS with this certificate chain, in PEM; needs --tls-key"+        <> value c.tlsCert+  tlsKey <-+    strOption $+      metavar "FILE"+        <> long "tls-key"+        <> helpEnv "TLS_KEY" "Private key of the certificate of --tls-cert, in PEM"+        <> value c.tlsKey+  noBloom <-+    switch $+      long "no-bloom"+        <> helpEnv "BLOOM=false" "No RocksDB Bloom filters on the data that is read by key, which are on by default"+  -- it was the way to turn them on, and commands that have it must still+  -- start+  _ <- switch (long "bloom" <> hidden)   maxOpenFiles <-     option auto $       metavar "COUNT"         <> long "max-open-files"-        <> help "RocksDB max_open_files; bounds index/filter memory (0 = unlimited)"+        <> helpEnv "MAX_OPEN_FILES" "RocksDB max_open_files; bounds index/filter memory (0 = unlimited)"         <> showDefault         <> value c.maxOpenFiles   writeBufferMB <-     option auto $       metavar "MB"         <> long "write-buffer-mb"-        <> help "RocksDB memtable budget across all column families in MiB (0 = unlimited)"+        <> helpEnv "WRITE_BUFFER_MB" "RocksDB memtable budget across all column families in MiB (0 = unlimited)"         <> showDefault         <> value c.writeBufferMB   maxWalMB <-     option auto $       metavar "MB"         <> long "max-wal-mb"-        <> help "RocksDB write-ahead log size in MiB that forces flushes so old log files can be deleted (0 = RocksDB default)"+        <> helpEnv+          "MAX_WAL_MB"+          ( unwords+              [ "RocksDB write-ahead log size in MiB that forces flushes so old",+                "log files can be deleted (0 = RocksDB default)"+              ]+          )         <> showDefault         <> value c.maxWalMB   blockCacheMB <-     option auto $       metavar "MB"         <> long "block-cache-mb"-        <> help "RocksDB block cache in MiB, shared by all column families, holding index and filter blocks too (0 = RocksDB default of 32 MiB per column family)"+        <> helpEnv+          "BLOCK_CACHE_MB"+          ( unwords+              [ "RocksDB block cache in MiB, shared by all column families,",+                "holding index and filter blocks too (0 = RocksDB default of",+                "32 MiB per column family)"+              ]+          )         <> showDefault         <> value c.blockCacheMB-  pure Config {..}+  pure Config {bloom = c.bloom && not noBloom, ..}  -- | Network by name. networkReader :: String -> Either String Network@@ -632,19 +676,18 @@ run :: Config -> IO () run cfg =   withContext $ \ctx ->-    withLogger level $ \logger -> flip runContT return $ do+    withLogger level $ \logger -> do       net <- either error return $ networkReader cfg.net-      stats <- ContT $ with_stats logger net-      let dir = cfg.dir </> net.name-      logInfo (withSource "haskoin-store" logger) $-        "Creating working directory (if not found): " <> cs dir-      createDirectoryIfMissing True dir-      store <- ContT $ run_store logger net ctx stats dir-      if cfg.noWeb-        then lift . withSubscription store.pub $ \sub ->-          loopForever (receive sub)-        else do-          lift $ run_web logger stats store+      with_stats logger net $ \stats -> do+        let dir = cfg.dir </> net.name+        logInfo (withSource "haskoin-store" logger) $+          "Creating working directory (if not found): " <> cs dir+        createDirectoryIfMissing True dir+        run_store logger net ctx stats dir $ \store ->+          if cfg.noWeb+            then withSubscription store.pub $ \sub ->+              loopForever (receive sub)+            else run_web logger stats store   where     run_store logger net ctx stats dir =       withStore@@ -656,10 +699,12 @@             net = net,             ctx = ctx,             redis = if cfg.redis then Just cfg.redisURL else Nothing,+            fleet = cfg.fleet,             gap = cfg.webLimits.xpubGap,             initGap = cfg.webLimits.xpubGapInit,             redisMinAddrs = cfg.redisMinAddrs,-            redisMaxKeys = cfg.redisMaxKeys,+            redisMinTxs = cfg.redisMinTxs,+            redisMaxXPubs = cfg.redisMaxXPubs,             wipeMempool = cfg.wipeMempool,             noMempool = cfg.noMempool,             syncMempool = cfg.syncMempool,@@ -669,7 +714,6 @@             maxPeerLife = fromIntegral cfg.maxPeerLife,             connect = withConnection,             stats = stats,-            redisSyncInterval = cfg.redisSyncInterval,             bloom = cfg.bloom,             maxOpenFiles = if cfg.maxOpenFiles > 0 then Just cfg.maxOpenFiles else Nothing,             writeBufferSize =@@ -705,6 +749,11 @@             noXPub = cfg.noXPub,             noBlockchainInfo = cfg.noBlockchainInfo,             healthCheckInterval = cfg.healthCheckInterval,+            tls = case (cfg.tlsCert, cfg.tlsKey) of+              ("", "") -> Nothing+              ("", _) -> error "--tls-key needs --tls-cert"+              (_, "") -> error "--tls-cert needs --tls-key"+              (certificate, key) -> Just (certificate, key),             logger = withSource "Web" logger           }     with_stats logger net go
bench/Main.hs view
@@ -1,9 +1,3 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}- -- | Load-test server for the web API. -- -- Syncs a synthetic regtest chain from an in-process mock peer into a fresh@@ -20,29 +14,28 @@ module Main (main) where  import Conduit-import Control.Monad (forever, unless)-import Control.Monad.Reader (runReaderT)-import Data.ByteString (ByteString)-import Data.ByteString qualified as B-import Data.HashMap.Strict qualified as HashMap-import Data.List (find) import Control.DeepSeq (NFData, force)+import Control.Monad (forever, unless) import Data.Aeson (toEncoding) import Data.Aeson.Encoding (encodingToLazyByteString)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B import Data.Default (def)+import Data.HashMap.Strict qualified as HashMap import Data.IntMap.Strict qualified as IntMap-import Data.Vector qualified as V-import GHC.Clock (getMonotonicTimeNSec)-import Haskoin.Store.Data+import Data.List (find) import Data.Maybe (fromMaybe, listToMaybe, mapMaybe) import Data.Serialize (decode, encode, runGet, runPut) import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Vector qualified as V import Data.Word (Word32)+import GHC.Clock (getMonotonicTimeNSec) import Haskoin import Haskoin.Node import Haskoin.Store+import Haskoin.Store.Database.Query qualified as Q import NQE import Network.Socket (SockAddr (..)) import System.Environment (getArgs)@@ -72,7 +65,7 @@         T.putStrLn $ "hot address: " <> addrText net (hotAddr ctx)         T.putStrLn $ "xpub: " <> xPubExport net ctx (benchXPub ctx)         T.putStrLn $ "xpub address: " <> addrText net (xpubAddr ctx 0 0)-        bh <- runReaderT getBestBlock store.db+        bh <- Q.getBestBlock store.db         T.putStrLn $ "best block: " <> maybe "" blockHashToHex bh         putStrLn $ "serving on port " <> show port         hFlush stdout@@ -83,7 +76,10 @@  waitHeight :: Store -> Inbox StoreEvent -> Int -> IO () waitHeight store sub n = do-  h <- maybe 0 (.height) <$> runReaderT (getBestBlock >>= maybe (return Nothing) getBlock) store.db+  h <-+    Q.getBestBlock store.db >>= \case+      Nothing -> return 0+      Just bh -> maybe 0 (.height) <$> Q.getBlock store.db bh   unless (fromIntegral h >= n) $ do     _ <- receiveMatch sub $ \case       StoreBestBlock b -> Just b@@ -104,9 +100,11 @@       gap = 32,       initGap = 20,       redisMinAddrs = 20,-      redisMaxKeys = 100 * 1000 * 1000,+      redisMinTxs = 1000,+      redisMaxXPubs = 10000,       noMempool = False,       wipeMempool = False,+      fleet = False,       syncMempool = False,       blockWindow = 64,       mempoolTimeout = 14,@@ -114,7 +112,6 @@       maxPeerLife = 48 * 3600,       connect = mockPeer blocks requests net ad,       stats = Nothing,-      redisSyncInterval = 30,       bloom = True,       maxOpenFiles = Nothing,       writeBufferSize = Nothing,@@ -147,6 +144,7 @@       noXPub = False,       noBlockchainInfo = False,       healthCheckInterval = 30,+      tls = Nothing,       logger = quiet     } @@ -161,7 +159,9 @@ xpubAddr ctx m i = fst (deriveAddr ctx (pubSubKey ctx (benchXPub ctx) m) i)  hotAddr :: Ctx -> Address-hotAddr ctx = pubKeyAddr ctx (derivePublicKey ctx (wrapSecKey True (fromMaybe (error "key") (secKey (B.replicate 32 0x42)))))+hotAddr ctx = pubKeyAddr ctx (derivePublicKey ctx (wrapSecKey True key))+  where+    key = fromMaybe (error "key") (secKey (B.replicate 32 0x42))  scriptOf :: Ctx -> Address -> ByteString scriptOf ctx = fromMaybe (error "script") . addressToScriptBS ctx
haskoin-store.cabal view
@@ -1,204 +1,362 @@-cabal-version: 1.12+cabal-version: 2.0  -- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack  name:           haskoin-store-version:        1.8.1+version:        1.9.0 synopsis:       Storage and index for Bitcoin and Bitcoin Cash-description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-store#readme>+description:    An indexer for Bitcoin and Bitcoin Cash. It downloads blocks and mempool+                transactions from the peer-to-peer network, indexes them in RocksDB, and+                serves a REST API with balances, transactions and unspent outputs of+                addresses and extended public keys, a Blockchain.info compatible API, and+                events over WebSockets. An optional Redis cache, shared by several stores,+                keeps large wallets fast. See the README for installation and+                configuration. category:       Bitcoin, Finance, Network-homepage:       http://github.com/jprupp/haskoin-store#readme-bug-reports:    http://github.com/jprupp/haskoin-store/issues+homepage:       https://github.com/jprupp/haskoin-store#readme+bug-reports:    https://github.com/jprupp/haskoin-store/issues author:         JP Rupp maintainer:     jprupp@protonmail.ch+copyright:      (c) Haskoin Developers license:        MIT license-file:   LICENSE build-type:     Simple+tested-with:+    GHC == 9.12.4+extra-source-files:+    src/Haskoin/Store/Cache/lua/bodies.lua+    src/Haskoin/Store/Cache/lua/commit.lua+    src/Haskoin/Store/Cache/lua/delete.lua+    src/Haskoin/Store/Cache/lua/evict.lua+    src/Haskoin/Store/Cache/lua/page.lua+    src/Haskoin/Store/Cache/lua/spends.lua+    src/Haskoin/Store/Cache/lua/unlock.lua+    src/Haskoin/Store/Fleet/lua/move.lua+    src/Haskoin/Store/Fleet/lua/pool.lua+    src/Haskoin/Store/Fleet/lua/read.lua+    src/Haskoin/Store/Fleet/lua/report.lua+    src/Haskoin/Store/Fleet/lua/xpub.lua+    stress/wrk.lua+    swagger.yaml+extra-doc-files:+    README.md+    CHANGELOG.md  source-repository head   type: git-  location: https://github.com/jprupp/haskoin-store.git+  location: https://github.com/jprupp/haskoin-store  library   exposed-modules:       Haskoin.Store       Haskoin.Store.BlockStore+      Haskoin.Store.BlockStore.Pending+      Haskoin.Store.BlockStore.Sync       Haskoin.Store.Cache+      Haskoin.Store.Cache.Protocol       Haskoin.Store.Common+      Haskoin.Store.Database.Mempool+      Haskoin.Store.Database.Migrate+      Haskoin.Store.Database.Query+      Haskoin.Store.Database.Query.Layered+      Haskoin.Store.Database.Query.Pooled       Haskoin.Store.Database.Reader       Haskoin.Store.Database.Types+      Haskoin.Store.Database.Views       Haskoin.Store.Database.Writer+      Haskoin.Store.Fleet+      Haskoin.Store.Fleet.Member+      Haskoin.Store.Fleet.Pool+      Haskoin.Store.Fleet.Redis+      Haskoin.Store.Fleet.XPub       Haskoin.Store.Logic+      Haskoin.Store.Logic.Layered       Haskoin.Store.Manager       Haskoin.Store.Web+      Haskoin.Store.Web.Docs+      Haskoin.Store.Web.Events+      Haskoin.Store.Web.Router   other-modules:       Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store   hs-source-dirs:       src+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:-      aeson >=1.4.7.1-    , aeson-pretty >=0.8.8-    , base >=4.9 && <5-    , base16 >=2.0-    , bytes >=0.17-    , bytestring >=0.10.10.0-    , cereal >=0.5.8.1-    , conduit >=1.3.2-    , containers >=0.6.2.1-    , data-default >=0.7.1.1-    , deepseq >=1.4.4.0-    , hashable >=1.3.0.0-    , hashtables >=1.2.4.2-    , haskoin-core >=1.6.1-    , haskoin-node >=2.3.0-    , haskoin-store-data ==1.8.1-    , hedis >=0.16.1-    , http-types >=0.12.3-    , lens >=4.18.1-    , monad-control >=1.0.2.3-    , mtl >=2.2.2-    , network >=3.1.1.1-    , nqe >=0.6.5-    , random >=1.1-    , rocksdb-haskell-jprupp >=2.6.0-    , rocksdb-query >=0.6.0-    , scotty >=0.22-    , statsd-rupp >=0.4-    , stm >=2.5.0.0-    , string-conversions >=0.4.0.1-    , text >=1.2.3.0-    , time >=1.9.3-    , transformers >=0.5.6.2-    , unliftio >=0.2.12.1-    , unordered-containers >=0.2.10.0-    , vault >=0.3.1.5-    , wai >=3.2.2.1-    , wai-websockets >=3.0.1.2-    , warp >=3.3.10-    , websockets >=0.13-    , wreq >=0.5.3.2-  default-language: Haskell2010+      aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , file-embed >=0.0.16.0 && <0.1+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , http-types >=0.12.6 && <0.13+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024 +library integration+  exposed-modules:+      Haskoin.Store.Integration.Image+      Haskoin.Store.Integration.Node+      Haskoin.Store.Integration.Podman+      Haskoin.Store.Integration.Valkey+  other-modules:+      Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store+  hs-source-dirs:+      integration+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , file-embed >=0.0.16.0 && <0.1+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , http-types >=0.12.6 && <0.13+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , process >=1.6.26.1 && <1.7+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024+ executable haskoin-store   main-is: Main.hs   other-modules:       Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store   hs-source-dirs:       app-  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts "-with-rtsopts=-N --disable-delayed-os-memory-return"   build-depends:-      aeson >=1.4.7.1-    , aeson-pretty >=0.8.8-    , base >=4.9 && <5-    , base16 >=2.0-    , bytes >=0.17-    , bytestring >=0.10.10.0-    , cereal >=0.5.8.1-    , conduit >=1.3.2-    , containers >=0.6.2.1-    , data-default >=0.7.1.1-    , deepseq >=1.4.4.0-    , filepath-    , hashable >=1.3.0.0-    , hashtables >=1.2.4.2-    , haskoin-core >=1.6.1-    , haskoin-node >=2.3.0+      aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , file-embed >=0.0.16.0 && <0.1+    , filepath >=1.5.5.0 && <1.6+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*     , haskoin-store-    , haskoin-store-data ==1.8.1-    , hedis >=0.16.1-    , http-types >=0.12.3-    , lens >=4.18.1-    , monad-control >=1.0.2.3-    , mtl >=2.2.2-    , network >=3.1.1.1-    , nqe >=0.6.5-    , optparse-applicative-    , random >=1.1-    , rocksdb-haskell-jprupp >=2.6.0-    , rocksdb-query >=0.6.0-    , scotty >=0.22-    , statsd-rupp >=0.4-    , stm >=2.5.0.0-    , string-conversions >=0.4.0.1-    , text >=1.2.3.0-    , time >=1.9.3-    , transformers >=0.5.6.2-    , unliftio >=0.2.12.1-    , unordered-containers >=0.2.10.0-    , vault >=0.3.1.5-    , wai >=3.2.2.1-    , wai-websockets >=3.0.1.2-    , warp >=3.3.10-    , websockets >=0.13-    , wreq >=0.5.3.2-  default-language: Haskell2010+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , http-types >=0.12.6 && <0.13+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , optparse-applicative >=0.19.0.0 && <0.20+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024  test-suite spec   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:-      Haskoin.Store.BlockStoreSpec+      Haskoin.Store.BlockStore.PendingSpec+      Haskoin.Store.BlockStore.SyncSpec+      Haskoin.Store.Cache.ProtocolSpec       Haskoin.Store.CacheSpec       Haskoin.Store.CommonSpec+      Haskoin.Store.Database.MigrateSpec+      Haskoin.Store.Database.Query.LayeredSpec+      Haskoin.Store.Database.ViewsSpec+      Haskoin.Store.Fleet.MemberSpec+      Haskoin.Store.Fleet.PoolSpec+      Haskoin.Store.Fleet.RedisSpec+      Haskoin.Store.Fleet.XPubSpec+      Haskoin.Store.FleetSpec+      Haskoin.Store.Histories+      Haskoin.Store.IntegrationSpec+      Haskoin.Store.Logic.LayeredSpec       Haskoin.Store.LogicSpec       Haskoin.Store.TestBlocks+      Haskoin.Store.Web.EventsSpec+      Haskoin.Store.Web.RouterSpec       Haskoin.Store.WebSpec       Haskoin.StoreSpec       Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store   hs-source-dirs:       test-  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts "-with-rtsopts=-N -T"+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts "-with-rtsopts=-N4 -T -M4g"   build-depends:-      QuickCheck >=2.13.2-    , aeson >=1.4.7.1-    , aeson-pretty >=0.8.8-    , base >=4.9 && <5-    , base16 >=2.0-    , base64 >=0.4.1-    , bytes >=0.17-    , bytestring >=0.10.10.0-    , cereal >=0.5.8.1-    , conduit >=1.3.2-    , containers >=0.6.2.1-    , data-default >=0.7.1.1-    , deepseq >=1.4.4.0-    , fast-logger >=3.0-    , hashable >=1.3.0.0-    , hashtables >=1.2.4.2-    , haskoin-core >=1.6.1-    , haskoin-node >=2.3.0+      QuickCheck >=2.17.1.0 && <2.18+    , aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , base64 >=1.0.1 && <1.1+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , fast-logger >=3.2.9 && <3.3+    , file-embed >=0.0.16.0 && <0.1+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*     , haskoin-store-    , haskoin-store-data ==1.8.1-    , hedis >=0.16.1-    , hspec >=2.7.1-    , http-types >=0.12.3-    , lens >=4.18.1-    , monad-control >=1.0.2.3-    , mtl >=2.2.2-    , network >=3.1.1.1-    , nqe >=0.6.5-    , random >=1.1-    , rocksdb-haskell-jprupp >=2.6.0-    , rocksdb-query >=0.6.0-    , scotty >=0.22-    , statsd-rupp >=0.4-    , stm >=2.5.0.0-    , string-conversions >=0.4.0.1-    , text >=1.2.3.0-    , time >=1.9.3-    , transformers >=0.5.6.2-    , unliftio >=0.2.12.1-    , unordered-containers >=0.2.10.0-    , vault >=0.3.1.5-    , wai >=3.2.2.1-    , wai-websockets >=3.0.1.2-    , warp >=3.3.10-    , websockets >=0.13-    , wreq >=0.5.3.2-  default-language: Haskell2010+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , hspec >=2.11.18 && <2.12+    , http-types >=0.12.6 && <0.13+    , integration+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , process >=1.6.26.1 && <1.7+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024   build-tool-depends: hspec-discover:hspec-discover  benchmark haskoin-store-bench@@ -206,51 +364,127 @@   main-is: Main.hs   other-modules:       Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store   hs-source-dirs:       bench+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N   build-depends:-      aeson >=1.4.7.1-    , aeson-pretty >=0.8.8-    , base >=4.9 && <5-    , base16 >=2.0-    , bytes >=0.17-    , bytestring >=0.10.10.0-    , cereal >=0.5.8.1-    , conduit >=1.3.2-    , containers >=0.6.2.1-    , data-default >=0.7.1.1-    , deepseq >=1.4.4.0-    , hashable >=1.3.0.0-    , hashtables >=1.2.4.2-    , haskoin-core >=1.6.1-    , haskoin-node >=2.3.0+      aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , file-embed >=0.0.16.0 && <0.1+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*     , haskoin-store-    , haskoin-store-data ==1.8.1-    , hedis >=0.16.1-    , http-types >=0.12.3-    , lens >=4.18.1-    , monad-control >=1.0.2.3-    , mtl >=2.2.2-    , network >=3.1.1.1-    , nqe >=0.6.5-    , random >=1.1-    , rocksdb-haskell-jprupp >=2.6.0-    , rocksdb-query >=0.6.0-    , scotty >=0.22-    , statsd-rupp >=0.4-    , stm >=2.5.0.0-    , string-conversions >=0.4.0.1-    , text >=1.2.3.0-    , time >=1.9.3-    , transformers >=0.5.6.2-    , unliftio >=0.2.12.1-    , unordered-containers >=0.2.10.0-    , vault >=0.3.1.5-    , vector-    , wai >=3.2.2.1-    , wai-websockets >=3.0.1.2-    , warp >=3.3.10-    , websockets >=0.13-    , wreq >=0.5.3.2-  default-language: Haskell2010+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , http-types >=0.12.6 && <0.13+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , vector >=0.13.2.0 && <0.14+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024++benchmark haskoin-store-stress+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_haskoin_store+  autogen-modules:+      Paths_haskoin_store+  hs-source-dirs:+      stress+  default-extensions:+      DuplicateRecordFields+      NoFieldSelectors+      OverloadedRecordDot+      OverloadedStrings+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N2+  build-depends:+      aeson >=2.2.5.1 && <2.3+    , aeson-pretty >=0.8.11 && <0.9+    , base >=4.21.2.0 && <4.22+    , base16 ==2.0.*+    , bytes >=0.17.5 && <0.18+    , bytestring >=0.12.2.0 && <0.13+    , case-insensitive >=1.2.1.0 && <1.3+    , cereal >=0.5.8.3 && <0.6+    , conduit >=1.3.6.1 && <1.4+    , containers ==0.7.*+    , data-default >=0.8.0.2 && <0.9+    , deepseq >=1.5.1.0 && <1.6+    , file-embed >=0.0.16.0 && <0.1+    , hashable >=1.5.1.0 && <1.6+    , hashtables >=1.4.2 && <1.5+    , haskoin-core >=1.6.1 && <1.7+    , haskoin-node ==2.5.*+    , haskoin-store+    , haskoin-store-data ==1.11.*+    , hedis >=0.16.4 && <0.17+    , http-types >=0.12.6 && <0.13+    , integration+    , lens >=5.3.6 && <5.4+    , monad-control >=1.0.3.1 && <1.1+    , mtl >=2.3.2 && <2.4+    , network >=3.2.12.0 && <3.3+    , nqe >=0.6.9 && <0.7+    , optparse-applicative >=0.19.0.0 && <0.20+    , process >=1.6.26.1 && <1.7+    , random >=1.3.1 && <1.4+    , rocksdb-haskell-jprupp >=2.7.1 && <2.8+    , rocksdb-query >=0.6.1 && <0.7+    , statsd-rupp >=0.5.0.2 && <0.6+    , stm >=2.5.3.1 && <2.6+    , string-conversions >=0.4.0.1 && <0.5+    , text >=2.1.4 && <2.2+    , time ==1.14.*+    , transformers >=0.6.3.0 && <0.7+    , unliftio >=0.2.25.1 && <0.3+    , unordered-containers >=0.2.21 && <0.3+    , vault >=0.3.2.0 && <0.4+    , wai >=3.2.5 && <3.3+    , wai-extra >=3.1.18 && <3.2+    , wai-websockets >=3.0.1.2 && <3.1+    , warp >=3.4.17 && <3.5+    , warp-tls >=3.4.14 && <3.5+    , websockets >=0.13.0.0 && <0.14+    , wreq >=0.5.4.5 && <0.6+  default-language: GHC2024
+ integration/Haskoin/Store/Integration/Image.hs view
@@ -0,0 +1,196 @@+-- |+-- Module      : Haskoin.Store.Integration.Image+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The container image of the integration tests, built from pinned and+-- verified sources only.+--+-- The tests use no third-party images. The image starts from the official+-- Ubuntu 26.04 image, pinned by the digest of its multi-architecture index,+-- installs Valkey (the Redis-compatible server Ubuntu ships) and wrk from the+-- Ubuntu archive at a fixed snapshot date (apt checks the signatures of the+-- archive; the certificates needed to reach the snapshot service over HTTPS+-- come from the regular archive first), and downloads Bitcoin Core and+-- Bitcoin Cash Node from their official release locations, checking each+-- tarball against a SHA-256 hash pinned here. A tarball that does not match+-- stops the build.+--+-- The hashes were taken from the signed checksum files of each release and+-- checked against the release signing keys of each project when they were+-- pinned:+--+-- * Bitcoin Core 32.1: @SHA256SUMS@ from bitcoincore.org, with good+--   signatures from eight of the builders whose keys are in the+--   @bitcoin-core/guix.sigs@ repository;+-- * Bitcoin Cash Node 29.2.0: @SHA256SUMS.29.2.0.asc.*@ from the+--   @bitcoin-cash-node/announcements@ repository, with good signatures from+--   two maintainers whose keys are in @contrib/gitian-signing/pubkeys.txt@.+--+-- To move to new releases, change the versions and hashes below after+-- checking them the same way. The tag of the image is a hash of the whole+-- recipe, so a change builds a new image and the old one is no longer used.+module Haskoin.Store.Integration.Image+  ( testImage,+    coreBin,+    bchnBin,+    valkeyBin,+    wrkBin,+  )+where++import Data.ByteString.Char8 qualified as C8+import Data.List (intercalate)+import Data.Serialize (encode)+import Data.Text qualified as T+import Haskoin (encodeHex, sha256)+import System.Exit (ExitCode (..))+import System.Process (readProcessWithExitCode)+import UnliftIO.Temporary (withSystemTempDirectory)++-- | Ubuntu 26.04, by the digest of its multi-architecture index.+base :: String+base = "docker.io/library/ubuntu@sha256:6a06111c3b0a8600feadb270aec771dc239bf407b25625f93f44ab00e11f4d3c"++-- | Date of the snapshot of the Ubuntu archive to install packages from, so+-- that the same versions are installed every time.+snapshot :: String+snapshot = "20261101T000000Z"++-- | Bitcoin Core release and the hashes of its Linux tarballs for x86_64 and+-- aarch64.+coreVersion, coreAmd64, coreArm64 :: String+coreVersion = "32.1"+coreAmd64 = "61545c17c3120fb7412f896d248b11993af04ec8817aad3649c55b2afb044963"+coreArm64 = "7ed37c7373eb6aa3e3f72842596a1afeba1b223717033cade9842b25773e1922"++-- | Bitcoin Cash Node release and the hashes of its Linux tarballs for x86_64+-- and aarch64.+bchnVersion, bchnAmd64, bchnArm64 :: String+bchnVersion = "29.2.0"+bchnAmd64 = "23bbe9ff749136a9b832a2a59023ce76a5391322c1ea2f4f24b5b91f035081bc"+bchnArm64 = "27e177e5a07dc78c2c0d18798ea2e3925111955509ecdc476e828e7ce8f46383"++-- | Daemon of Bitcoin Core in the image.+coreBin :: String+coreBin = "/opt/bitcoin-core/bin/bitcoind"++-- | Daemon of Bitcoin Cash Node in the image.+bchnBin :: String+bchnBin = "/opt/bitcoin-cash-node/bin/bitcoind"++-- | Valkey server in the image.+valkeyBin :: String+valkeyBin = "/usr/bin/valkey-server"++-- | The HTTP load generator wrk in the image, for the stress test of the+-- cache.+wrkBin :: String+wrkBin = "/usr/bin/wrk"++-- | Recipe of the image.+--+-- Every download is its own layer, ordered from the one that changes least+-- often: the packages of the snapshot, then Bitcoin Core, then Bitcoin Cash+-- Node, then wrk for the stress test, which came last. Podman keeps the+-- layers it builds and reuses each one whose instruction, and those before+-- it, did not change, so moving one node to a new release downloads only+-- that release. Nothing in the image depends on the time it is built.+--+-- The recipe stays here rather than in a Containerfile of its own: it is+-- made from the versions and hashes defined in this module, and the tag of+-- the image is a hash of it, so the tests and the stress program, which may+-- run from any directory, need no file to find. It is written one+-- instruction and one shell command per line.+containerfile :: String+containerfile =+  unlines $+    [ "FROM " <> base,+      "ARG TARGETARCH",+      -- the snapshot archive is only served over HTTPS, so first get the+      -- certificates to reach it from the regular archive (over HTTP, with+      -- the packages checked by the signatures of the archive), then install+      -- everything, the certificates included, from the snapshot+      "RUN apt-get update --error-on=any \\",+      " && apt-get install -y --no-install-recommends ca-certificates \\",+      " && apt-get update --error-on=any --snapshot " <> snapshot <> " \\",+      " && apt-get install -y --no-install-recommends --snapshot "+        <> snapshot+        <> " ca-certificates curl valkey-server \\",+      " && rm -rf /var/lib/apt/lists/*"+    ]+      <> release+        "/opt/bitcoin-core"+        ( "https://bitcoincore.org/bin/bitcoin-core-"+            <> coreVersion+            <> "/bitcoin-"+            <> coreVersion+            <> "-$a-linux-gnu.tar.gz"+        )+        coreAmd64+        coreArm64+      <> release+        "/opt/bitcoin-cash-node"+        ( "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v"+            <> bchnVersion+            <> "/bitcoin-cash-node-"+            <> bchnVersion+            <> "-$a-linux-gnu.tar.gz"+        )+        bchnAmd64+        bchnArm64+      <> [ "RUN apt-get update --error-on=any --snapshot " <> snapshot <> " \\",+           " && apt-get install -y --no-install-recommends --snapshot " <> snapshot <> " wrk \\",+           " && rm -rf /var/lib/apt/lists/*"+         ]+  where+    -- download a release for the architecture of the image, check its hash,+    -- and unpack it; @$a@ in the URL stands for the architecture+    release dir url amd64 arm64 =+      [ "RUN set -eu; \\",+        " case \"$TARGETARCH\" in \\",+        "  amd64) a=x86_64; h=" <> amd64 <> ";; \\",+        "  arm64) a=aarch64; h=" <> arm64 <> ";; \\",+        "  *) echo \"unsupported architecture $TARGETARCH\"; exit 1;; \\",+        " esac; \\",+        " curl -fsSL -o /tmp/release.tar.gz \"" <> url <> "\"; \\",+        " echo \"$h  /tmp/release.tar.gz\" | sha256sum -c -; \\",+        " mkdir -p " <> dir <> "; \\",+        " tar -xzf /tmp/release.tar.gz -C " <> dir <> " --strip-components=1; \\",+        " rm /tmp/release.tar.gz"+      ]++-- | Tag of the image: a hash of its recipe.+tag :: String+tag =+  "localhost/haskoin-store-test:"+    <> take 16 (T.unpack (encodeHex (encode (sha256 (C8.pack containerfile)))))++-- | Build the image unless it exists already, and return its tag. The first+-- build downloads the base image, the packages and the two releases, and may+-- take a few minutes; later builds of a changed recipe reuse the layers that+-- did not change (see the recipe above). Images and layers are never removed by+-- the tests. The build may use 1 GiB of memory.+testImage :: IO String+testImage = do+  (exists, _, _) <- readProcessWithExitCode "podman" ["image", "exists", tag] ""+  case exists of+    ExitSuccess -> return tag+    _ -> withSystemTempDirectory "haskoin-store-image" $ \ctx -> do+      (code, out, err) <-+        readProcessWithExitCode+          "podman"+          ["build", "--layers", "--memory", "1g", "--tag", tag, "--file", "-", ctx]+          containerfile+      case code of+        ExitSuccess -> return tag+        _ ->+          fail $+            intercalate+              "\n"+              ["Could not build the test image " <> tag <> ":", lastLines out, lastLines err]+  where+    lastLines = unlines . reverse . take 30 . reverse . lines
+ integration/Haskoin/Store/Integration/Node.hs view
@@ -0,0 +1,237 @@+-- |+-- Module      : Haskoin.Store.Integration.Node+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Regtest nodes for the integration tests: Bitcoin Core for Bitcoin and+-- Bitcoin Cash Node for Bitcoin Cash, each in a container, driven through+-- JSON-RPC.+--+-- A regtest node gives the tests a chain they fully control: they mine blocks+-- on demand, send transactions from the wallet of the node, and sign+-- transactions that spend from the keys of the test xpubs. The store under+-- test connects to the node over the peer-to-peer protocol, as it would to+-- the real networks.+--+-- Only RPC calls that both nodes have are used. Bitcoin Cash Node has no+-- @generateblock@, so the reorganisation test is built on coinbase maturity+-- instead (see "Haskoin.Store.IntegrationSpec").+module Haskoin.Store.Integration.Node+  ( NodeKind (..),+    Node (..),+    nodeContainer,+    nodeNetwork,+    RpcError (..),+    rpc,+    waitRpc,+    ensureWallet,+    field,+    textField,+    btc,+    sats,+    getNewAddress,+    generateTo,+    sendTo,+    sendMany,+    getRawTx,+    bestBlockHash,+    rawMempool,+  )+where++import Control.Lens ((&), (?~), (^.))+import Control.Monad (void)+import Data.Aeson (Value (..), eitherDecode, object, (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Foldable (toList)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Word (Word64)+import Haskoin (Network, bchRegTest, btcRegTest)+import Haskoin.Store.Integration.Image (bchnBin, coreBin)+import Haskoin.Store.Integration.Podman (ContainerSpec (..))+import Network.Wreq qualified as Wreq+import UnliftIO (Exception, throwIO, tryAny)+import UnliftIO.Concurrent (threadDelay)++-- | Which node software.+data NodeKind = BitcoinCore | BitcoinCashNode+  deriving (Eq, Show)++-- | A node and how to reach it.+data Node = Node+  { kind :: !NodeKind,+    -- | URL of the JSON-RPC interface+    url :: !String,+    -- | port of the peer-to-peer interface on 127.0.0.1+    p2p :: !Int+  }++-- | Network constants of the regtest chain of a node.+nodeNetwork :: NodeKind -> Network+nodeNetwork BitcoinCore = btcRegTest+nodeNetwork BitcoinCashNode = bchRegTest++-- | Container of a regtest node, from the test image+-- ("Haskoin.Store.Integration.Image"): its chain is in a tmpfs of 512 MiB,+-- it may use 1 GiB of memory, and its caches and mempool are kept small.+--+-- Every peer is whitelisted, so that the store may ask for the mempool and+-- gets transactions relayed without delay.+nodeContainer :: NodeKind -> String -> ContainerSpec+nodeContainer kind image =+  ContainerSpec+    { name = case kind of+        BitcoinCore -> "bitcoin-core"+        BitcoinCashNode -> "bitcoin-cash-node",+      image = image,+      options =+        [ "--memory",+          "1g",+          "--tmpfs",+          "/chain:rw,size=512m,mode=1777",+          "--entrypoint",+          case kind of+            BitcoinCore -> coreBin+            BitcoinCashNode -> bchnBin+        ],+      command =+        [ "-regtest",+          "-datadir=/chain",+          "-printtoconsole",+          "-server",+          "-rpcuser=test",+          "-rpcpassword=test",+          "-rpcbind=0.0.0.0",+          "-rpcallowip=0.0.0.0/0",+          "-bind=0.0.0.0",+          "-listen=1",+          "-whitelist=0.0.0.0/0",+          "-txindex=1",+          "-fallbackfee=0.0002",+          "-dbcache=16",+          "-maxmempool=16"+        ]+          <> case kind of+            -- releases of Bitcoin Cash Node stop serving RPC some months+            -- after they come out unless told not to+            BitcoinCashNode -> ["-expire=0"]+            BitcoinCore -> [],+      ports = [18443, 18444]+    }++-- | Error returned by a node.+data RpcError = RpcError !Text !Value+  deriving (Show)++instance Exception RpcError++-- | Call a method of the node and return its result.+rpc :: Node -> Text -> [Value] -> IO Value+rpc node method params = do+  let opts =+        Wreq.defaults+          & Wreq.auth ?~ Wreq.basicAuth "test" "test"+          & Wreq.checkResponse ?~ (\_ _ -> return ())+      body =+        object+          [ "jsonrpc" .= ("1.0" :: Text),+            "id" .= ("test" :: Text),+            "method" .= method,+            "params" .= params+          ]+  r <- Wreq.postWith opts node.url body+  case eitherDecode (r ^. Wreq.responseBody) of+    Left e -> fail ("Bad answer to " <> T.unpack method <> ": " <> e)+    Right v -> case (field "error" v, field "result" v) of+      (Just Null, Just x) -> return x+      (Nothing, Just x) -> return x+      (Just e, _) -> throwIO (RpcError method e)+      _ -> fail ("No result in answer to " <> T.unpack method)++-- | Wait until the node answers RPC calls, for up to 90 seconds.+waitRpc :: Node -> IO ()+waitRpc node = go (360 :: Int)+  where+    go n =+      tryAny (rpc node "getblockchaininfo" []) >>= \case+        Right _ -> return ()+        Left e+          | n <= 0 -> fail ("The node did not start: " <> show e)+          | otherwise -> threadDelay 250000 >> go (n - 1)++-- | Make sure the node has exactly one wallet loaded, so that wallet calls+-- need no wallet name. Bitcoin Cash Node loads a default wallet; Bitcoin Core+-- has none until one is created.+ensureWallet :: Node -> IO ()+ensureWallet node =+  rpc node "listwallets" [] >>= \case+    Array ws | not (null ws) -> return ()+    _ -> void $ rpc node "createwallet" [String "test"]++-- | Field of a JSON object.+field :: Text -> Value -> Maybe Value+field k (Object o) = KeyMap.lookup (Key.fromText k) o+field _ _ = Nothing++-- | Text field of a JSON object.+textField :: Text -> Value -> Maybe Text+textField k v = case field k v of+  Just (String t) -> Just t+  _ -> Nothing++-- | Amount in satoshi as a JSON number of coins.+btc :: Word64 -> Value+btc s = Number (fromIntegral s / 100000000)++-- | JSON number of coins in satoshi.+sats :: Value -> Word64+sats (Number n) = round (n * 100000000)+sats v = error ("Not an amount: " <> show v)++asText :: Value -> IO Text+asText (String t) = return t+asText v = fail ("Expected text, got " <> show v)++-- | New address of the wallet of the node.+getNewAddress :: Node -> IO Text+getNewAddress node = rpc node "getnewaddress" [] >>= asText++-- | Mine blocks paying to an address. Returns their hashes.+generateTo :: Node -> Int -> Text -> IO [Text]+generateTo node n addr =+  rpc node "generatetoaddress" [Number (fromIntegral n), String addr] >>= \case+    Array hs -> mapM asText (toList hs)+    v -> fail ("Unexpected answer to generatetoaddress: " <> show v)++-- | Send from the wallet of the node to an address. Returns the transaction+-- hash.+sendTo :: Node -> Text -> Word64 -> IO Text+sendTo node addr amount =+  rpc node "sendtoaddress" [String addr, btc amount] >>= asText++-- | Send from the wallet of the node to several addresses in one+-- transaction. Returns the transaction hash.+sendMany :: Node -> [(Text, Word64)] -> IO Text+sendMany node outs =+  rpc node "sendmany" [String "", object [Key.fromText a .= btc v | (a, v) <- outs]]+    >>= asText++-- | Decoded transaction.+getRawTx :: Node -> Text -> IO Value+getRawTx node txid = rpc node "getrawtransaction" [String txid, Bool True]++-- | Hash of the best block.+bestBlockHash :: Node -> IO Text+bestBlockHash node = rpc node "getbestblockhash" [] >>= asText++-- | Hashes of the transactions in the mempool.+rawMempool :: Node -> IO [Text]+rawMempool node =+  rpc node "getrawmempool" [] >>= \case+    Array ts -> mapM asText (toList ts)+    v -> fail ("Unexpected answer to getrawmempool: " <> show v)
+ integration/Haskoin/Store/Integration/Podman.hs view
@@ -0,0 +1,190 @@+-- |+-- Module      : Haskoin.Store.Integration.Podman+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Containers for the integration tests, run with Podman.+--+-- The integration tests need real nodes and a real Redis, which are easiest+-- to get, at pinned versions and without installing anything, as container+-- images. Podman runs them without a daemon and without root, so a developer+-- or a CI runner can use it as it is.+--+-- Everything is kept off the disk and cleaned up:+--+-- * containers run with @--rm@ and are removed with @podman rm --force@ when+--   the test that started them ends, even if it fails;+-- * the volumes that images declare are mounted as tmpfs+--   (@--image-volume tmpfs@), and data directories are tmpfs mounts of+--   limited size;+-- * every container has a memory limit;+-- * containers carry a label with the process ID of the test run, and+--   containers left by a test run that was killed (whose process is gone) are+--   removed before new ones start.+--+-- Ports are published on random ports of the loopback interface, so several+-- test runs can share a machine.+module Haskoin.Store.Integration.Podman+  ( podmanUsable,+    ContainerSpec (..),+    Container (..),+    withContainer,+    hostPort,+    withRamDir,+  )+where++import Control.Monad (forM_, unless, void)+import Data.Aeson qualified as A+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.ByteString.Char8 qualified as C8+import Data.ByteString.Lazy qualified as BL+import Data.Char (isDigit, isSpace)+import Data.List (dropWhileEnd)+import Data.Text qualified as T+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.IO (hPutStrLn, stderr)+import System.Process (getCurrentPid, readProcessWithExitCode)+import UnliftIO (bracket, onException, timeout, tryAny)+import UnliftIO.Directory (doesDirectoryExist, doesPathExist, getTemporaryDirectory)+import UnliftIO.Temporary (withTempDirectory)++-- | Whether Podman can run containers here, or why not. Set+-- @HASKOIN_STORE_NO_PODMAN@ to a value other than @0@ to skip the tests that+-- need it.+podmanUsable :: IO (Either String ())+podmanUsable = do+  skip <- lookupEnv "HASKOIN_STORE_NO_PODMAN"+  case skip of+    Just v | v `notElem` ["", "0"] -> return (Left "HASKOIN_STORE_NO_PODMAN is set")+    _ -> do+      r <-+        tryAny . timeout (60 * 1000 * 1000) $+          readProcessWithExitCode "podman" ["info", "--format", "{{.Host.Arch}}"] ""+      -- containers left by a test run that was killed are removed before+      -- anything else, even if this run fails before starting any+      case r of+        Right (Just (ExitSuccess, _, _)) -> removeStale+        _ -> return ()+      return $ case r of+        Left e -> Left ("Podman is not available: " <> show e)+        Right Nothing -> Left "podman info did not answer within a minute"+        Right (Just (ExitSuccess, _, _)) -> Right ()+        Right (Just (_, _, err)) -> Left ("Podman cannot run containers: " <> trim err)++-- | What to run.+data ContainerSpec = ContainerSpec+  { -- | short name, part of the container name+    name :: !String,+    -- | image, with a pinned tag+    image :: !String,+    -- | options of @podman run@, such as memory limits and tmpfs mounts+    options :: ![String],+    -- | command and arguments, after the image+    command :: ![String],+    -- | TCP ports to publish on random ports of 127.0.0.1+    ports :: ![Int]+  }++-- | A running container.+data Container = Container+  { -- | container ID+    cid :: !String,+    -- | container name+    name :: !String+  }++-- | Label on every container started by the tests. Its value is the process+-- ID of the test run.+testLabel :: String+testLabel = "haskoin-store-test"++-- | Run a container while an action runs, and remove it afterwards. If the+-- action throws, the last lines of the log of the container are printed to+-- help find out why.+withContainer :: ContainerSpec -> (Container -> IO a) -> IO a+withContainer spec act = do+  removeStale+  pid <- show <$> getCurrentPid+  let cname = testLabel <> "-" <> pid <> "-" <> spec.name+      args =+        ["run", "--detach", "--rm", "--name", cname]+          <> ["--label", testLabel <> "=" <> pid, "--image-volume", "tmpfs"]+          <> spec.options+          <> concatMap (\p -> ["--publish", "127.0.0.1::" <> show p]) spec.ports+          <> [spec.image]+          <> spec.command+  bracket (start cname args) stop $ \c -> act c `onException` logs c+  where+    start cname args = do+      (code, out, err) <- readProcessWithExitCode "podman" args ""+      case code of+        ExitSuccess -> return Container {cid = trim out, name = cname}+        _ -> do+          -- a container may exist even though the command failed+          void $ readProcessWithExitCode "podman" ["rm", "--force", cname] ""+          fail ("podman run " <> spec.image <> " failed: " <> trim err)+    stop :: Container -> IO ()+    stop c =+      void $ readProcessWithExitCode "podman" ["rm", "--force", "--time", "2", c.cid] ""+    logs :: Container -> IO ()+    logs c = do+      (_, out, err) <- readProcessWithExitCode "podman" ["logs", "--tail", "40", c.cid] ""+      hPutStrLn stderr $ "--- last lines of the log of " <> c.name <> " ---\n" <> out <> err++-- | Remove the containers of test runs whose process is gone. The list+-- comes as JSON: the Go templates of @podman ps@ cannot index the labels in+-- every version of Podman.+removeStale :: IO ()+removeStale = do+  (code, out, _) <-+    readProcessWithExitCode+      "podman"+      ["ps", "--all", "--filter", "label=" <> testLabel, "--format", "json"]+      ""+  case (code, A.decode (BL.fromStrict (C8.pack out))) of+    (ExitSuccess, Just cs) ->+      forM_ (cs :: [A.Value]) $ \c -> case staleOf c of+        Just (cid, pid) | all isDigit pid -> do+          alive <- doesPathExist ("/proc/" <> pid)+          unless alive . void $ readProcessWithExitCode "podman" ["rm", "--force", cid] ""+        _ -> return ()+    _ -> return ()+  where+    staleOf c = do+      A.Object o <- Just c+      A.String cid <- KeyMap.lookup "Id" o+      A.Object labels <- KeyMap.lookup "Labels" o+      A.String pid <- KeyMap.lookup (Key.fromString testLabel) labels+      return (T.unpack cid, T.unpack pid)++-- | Port of the host where a port of a container is published.+hostPort :: Container -> Int -> IO Int+hostPort c p = do+  (code, out, err) <- readProcessWithExitCode "podman" ["port", c.cid, show p <> "/tcp"] ""+  case (code, lines out) of+    (ExitSuccess, l : _)+      | [(n, "")] <- reads (reverse (takeWhile (/= ':') (reverse (trim l)))) ->+          return n+    _ -> fail ("Cannot find the published port " <> show p <> " of " <> c.name <> ": " <> trim err)++trim :: String -> String+trim = dropWhileEnd isSpace . dropWhile isSpace++-- | A temporary directory in memory: under @/dev/shm@ if there is one, else+-- under @$XDG_RUNTIME_DIR@, else the usual temporary directory.+withRamDir :: (FilePath -> IO a) -> IO a+withRamDir f = do+  shm <- doesDirectoryExist "/dev/shm"+  xdg <- lookupEnv "XDG_RUNTIME_DIR"+  tmp <- getTemporaryDirectory+  let base+        | shm = "/dev/shm"+        | Just x <- xdg = x+        | otherwise = tmp+  withTempDirectory base "haskoin-store-test" f
+ integration/Haskoin/Store/Integration/Valkey.hs view
@@ -0,0 +1,57 @@+-- |+-- Module      : Haskoin.Store.Integration.Valkey+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- A Redis server for the integration tests and the stress test: Valkey, the+-- Redis-compatible server that Ubuntu ships, from the test image.+module Haskoin.Store.Integration.Valkey+  ( redisContainer,+    waitRedis,+  )+where++import Database.Redis qualified as Redis+import Haskoin.Store.Integration.Image (valkeyBin)+import Haskoin.Store.Integration.Podman (ContainerSpec (..))+import UnliftIO (bracket, tryAny)+import UnliftIO.Concurrent (threadDelay)++-- | Valkey, the Redis-compatible server of Ubuntu, from the test image, listening on every interface of the container+-- (only its published port is reachable, on 127.0.0.1), with nothing saved+-- to disk and at most 128 MiB of data.+redisContainer :: String -> ContainerSpec+redisContainer image =+  ContainerSpec+    { name = "redis",+      image = image,+      options = ["--memory", "256m", "--entrypoint", valkeyBin],+      command =+        [ "--bind",+          "0.0.0.0",+          "--protected-mode",+          "no",+          "--save",+          "",+          "--appendonly",+          "no",+          "--maxmemory",+          "128mb"+        ],+      ports = [6379]+    }++-- | Wait until Redis answers, for up to 30 seconds.+waitRedis :: String -> IO ()+waitRedis url = go (120 :: Int)+  where+    info = either error id (Redis.parseConnectInfo url)+    go n =+      tryAny (bracket (Redis.checkedConnect info) Redis.disconnect (const (return ()))) >>= \case+        Right () -> return ()+        Left e+          | n <= 0 -> fail ("Redis did not start: " <> show e)+          | otherwise -> threadDelay 250000 >> go (n - 1)
src/Haskoin/Store.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE DuplicateRecordFields #-}- -- | -- Module      : Haskoin.Store -- Copyright   : (c) Haskoin Developers@@ -14,33 +12,49 @@ -- <https://hackage.haskell.org/package/haskoin-node haskoin-node>, downloads -- blocks and mempool transactions, and indexes them in RocksDB. The indices -- answer questions about blocks, transactions, address balances, unspent--- outputs and extended public keys. An optional Redis cache speeds up queries--- about extended public keys with many addresses. A REST API built on these--- indices is in "Haskoin.Store.Web".+-- outputs and extended public keys. A REST API built on these indices is in+-- "Haskoin.Store.Web". (The Redis cache of extended public keys is being+-- rewritten; until it is back, every query reads the database.) -- -- = Running a store ----- 'withStore' starts everything from a 'StoreConfig': the database, the node,--- the block store that imports data, and the cache if configured. The 'Store'--- it returns has a 'DatabaseReader' and, if the cache is on, a 'CacheConfig'.--- Run queries with 'withDB', or inside 'withCache' to use the cache.+-- 'withStore' starts everything from a 'StoreConfig': the database, the node+-- and the block store that imports data. The 'Store' it returns has a+-- 'DatabaseReader'; query it with the functions of+-- "Haskoin.Store.Database.Query", which take the reader and run in 'IO'. -- -- @ -- 'withStore' cfg $ \\store ->---   'withSubscription' store.pub $ \\events -> do---     best <- 'withDB' store.db 'getBestBlock'+--   withSubscription store.pub $ \\events -> do+--     best <- Haskoin.Store.Database.Query.getBestBlock store.db --     ... -- @ -- -- = Modules -- -- * "Haskoin.Store.Data": types of the data returned by the store and the API.--- * "Haskoin.Store.Common": the 'StoreReadBase' and 'StoreReadExtra' query---   interfaces.--- * "Haskoin.Store.Database.Reader": queries against RocksDB.+-- * "Haskoin.Store.Common": helpers for paging, ordering and deriving+--   addresses, and 'StoreRead', the queries of a store as a record of+--   functions in 'IO'.+-- * "Haskoin.Store.Database.Reader": opening the RocksDB database.+-- * "Haskoin.Store.Database.Query": queries against RocksDB, as plain+--   functions.+-- * "Haskoin.Store.Database.Mempool", "Haskoin.Store.Database.Migrate",+--   "Haskoin.Store.Logic.Layered" and+--   "Haskoin.Store.Database.Query.Layered": a layout of the database in+--   which the mempool is kept apart from the chain, the migration to it, the+--   rules that import blocks and transactions in it, and its queries. The+--   store does not use them yet; see @docs\/layered-database.md@.+-- * "Haskoin.Store.Fleet": how stores that share a Redis agree on the block+--   that they all answer from, as pure decisions, and+--   "Haskoin.Store.Fleet.Redis", which carries them out.+--   "Haskoin.Store.Fleet.Pool" has the rules of the mempool that they share.+--   Nothing uses them yet.+-- * "Haskoin.Store.Database.Views": the database as it was at each recent+--   block, for a store that answers from the block its fleet agrees on.+--   Nothing uses it yet. -- * "Haskoin.Store.BlockStore": the process that imports blocks and --   transactions.--- * "Haskoin.Store.Cache": the Redis cache for extended public keys. -- * "Haskoin.Store.Web": the REST API server. -- = Purpose --@@ -64,9 +78,11 @@ --   query is a seek and a short scan, newest first; see --   "Haskoin.Store.Database.Types". ----- * __One query interface, several back ends.__ The database, the import buffer---   and the Redis cache all implement 'StoreReadBase' and 'StoreReadExtra', so---   the same query code runs against any of them.+-- * __Plain functions in 'IO'.__ Queries take the database reader, or a+--   'StoreRead' record of them, which the web server builds with the Redis+--   cache in front of the queries about xpubs. Nothing runs in a stack of+--   monad transformers, except the conduits that merge sorted streams. The+--   web server runs on WAI with a router of its own. -- -- * __The mempool is a first-class citizen.__ Unconfirmed transactions are indexed --   like confirmed ones, ordered before them, and kept across restarts, because@@ -84,16 +100,15 @@     module Haskoin.Store.Data,      -- * Cache+    Cache,     CacheConfig (..),-    CacheT,-    CacheError (..),-    withCache,-    connectRedis,-    isInCache,+    cacheIsCached,+    cacheDelete,+    cacheEvict,      -- * Store Reader-    StoreReadBase (..),-    StoreReadExtra (..),+    StoreRead (..),+    databaseRead,     Limits (..),     Start (..), @@ -110,6 +125,8 @@ import Haskoin.Store.Cache import Haskoin.Store.Common import Haskoin.Store.Data+-- the instances of the store classes for the database reader+import Haskoin.Store.Database.Query (databaseRead) import Haskoin.Store.Database.Reader import Haskoin.Store.Database.Types import Haskoin.Store.Manager
src/Haskoin/Store/BlockStore.hs view
@@ -1,1502 +1,1221 @@ {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}-{-# OPTIONS_GHC -Wno-ambiguous-fields #-}---- |--- Module      : Haskoin.Store.BlockStore--- Copyright   : (c) Haskoin Developers--- License     : MIT--- Maintainer  : jprupp@protonmail.ch--- Stability   : experimental--- Portability : POSIX------ The block store process downloads blocks and transactions from peers and--- imports them into the database.------ = Blocks------ When the best header of the node is ahead of the best block in the database,--- the block store picks one online peer as its syncing peer and asks it for--- blocks in batches of 'blockWindow'. Each block is imported as soon as it--- arrives. If the best block of the database is no longer in the main chain of--- headers, blocks are reverted until it is. A syncing peer that sends a block--- that was not requested, cannot find a block, or stays silent for longer than--- 'peerTimeout' is disconnected.------ = Mempool------ Once the database is in sync, the block store asks each peer for its mempool--- (if 'syncMempool' is set) and requests every transaction that peers--- announce. Received transactions are queued and imported in batches on every--- clock tick, in dependency order. Transactions that spend unknown outputs are--- kept for up to an hour, waiting for their parents. Mempool transactions older--- than 'mempoolTimeout' days are deleted.------ = Feeding the block store------ The block store does not listen to the node by itself.--- "Haskoin.Store.Manager" forwards node events to it with the--- @blockStore*@ functions of this module.--- = Why it works this way------ * __Blocks come from one peer at a time.__ Blocks must be imported in order,---   so downloading from several peers at once would only buffer blocks that cannot---   be imported yet. One syncing peer, kept busy with a sliding window of---   requests, uses the network well without unbounded buffering.------ * __The window refills early.__ The next batch is requested when fewer than a---   quarter of the window are still outstanding, so the peer never sits idle---   while the store imports, and the backlog never grows much beyond one window.------ * __Mempool sync waits for the tip.__ Unconfirmed transactions only make sense---   against the current chain, so they are requested once the database is in---   sync.------ * __Orphans get a grace period.__ A transaction can arrive before its parent.---   It is held for up to an hour, waiting for the parent, instead of being---   dropped and requested again.------ * __Imports are batched.__ Mempool transactions are imported up to 2000 at a---   time on each clock tick, which amortises the cost of a database write over---   many transactions during bursts.-module Haskoin.Store.BlockStore-  ( -- * Block Store-    BlockStore,-    BlockStoreConfig (..),-    withBlockStore,-    blockStorePeerConnect,-    blockStorePeerConnectSTM,-    blockStorePeerDisconnect,-    blockStorePeerDisconnectSTM,-    blockStoreHead,-    blockStoreHeadSTM,-    blockStoreBlock,-    blockStoreBlockSTM,-    blockStoreNotFound,-    blockStoreNotFoundSTM,-    blockStoreTx,-    blockStoreTxSTM,-    blockStoreTxHash,-    blockStoreTxHashSTM,-    blockStorePendingTxs,-    blockStorePendingTxsSTM,-    needsMoreBlocks,-  )-where--import Control.Monad-  ( forM,-    forM_,-    mzero,-    unless,-    void,-    when,-  )-import Control.Monad.Reader-  ( MonadReader,-    ReaderT (..),-    ask,-    asks,-  )-import Control.Monad.Trans (lift)-import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT)-import Data.Bool (bool)-import Data.ByteString qualified as B-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as HashMap-import Data.HashSet (HashSet)-import Data.HashSet qualified as HashSet-import Data.List (delete)-import Data.Maybe-  ( catMaybes,-    fromJust,-    fromMaybe,-    isJust,-    mapMaybe,-  )-import Data.Serialize (encode)-import Data.String (fromString)-import Data.String.Conversions (cs)-import Data.Text (Text)-import Data.Text qualified as T-import Data.Time.Clock-  ( NominalDiffTime,-    UTCTime,-    diffUTCTime,-    getCurrentTime,-  )-import Data.Time.Clock.POSIX-  ( getPOSIXTime,-    posixSecondsToUTCTime,-    utcTimeToPOSIXSeconds,-  )-import Data.Time.Format-  ( defaultTimeLocale,-    formatTime,-  )-import Haskoin-  ( Block (..),-    BlockHash (..),-    BlockHeader (..),-    BlockHeight,-    BlockNode (..),-    Ctx,-    GetData (..),-    InvType (..),-    InvVector (..),-    Message (..),-    Network (..),-    OutPoint (..),-    Tx (..),-    TxHash (..),-    TxIn (..),-    blockHashToHex,-    headerHash,-    txHash,-    txHashToHex,-  )-import Haskoin.Node-  ( Chain,-    LogLevel (..),-    Logger,-    OnlinePeer (..),-    Peer (..),-    PeerMgr,-    chainBlockMain,-    chainGetAncestor,-    chainGetBest,-    chainGetBlock,-    chainGetParents,-    getPeers,-    killPeer,-    logAt,-    sendMessage,-    setBusy,-    setFree,-  )-import Haskoin.Store.Common-import Haskoin.Store.Data-import Haskoin.Store.Database.Reader-import Haskoin.Store.Database.Writer-import Haskoin.Store.Logic-  ( ImportException (Orphan),-    ImportM,-    deleteUnconfirmedTx,-    importBlock,-    initBest,-    newMempoolTx,-    revertBlock,-  )-import NQE-  ( InChan,-    Listen,-    Mailbox,-    Publisher,-    inboxToMailbox,-    newInbox,-    publish,-    query,-    receive,-    send,-    sendSTM,-  )-import System.Metrics.StatsD-import System.Random (randomRIO)-import UnliftIO-  ( Exception,-    MonadIO,-    MonadUnliftIO,-    STM,-    TVar,-    async,-    atomically,-    catch,-    liftIO,-    link,-    modifyTVar,-    modifyTVar',-    newTVarIO,-    readTVar,-    readTVarIO,-    throwIO,-    try,-    withAsync,-    writeTVar,-  )-import UnliftIO.Concurrent (threadDelay)---- | Messages handled by the block store process.-data BlockStoreMessage-  = -- | the node has a new best header-    BlockNewBest !BlockNode-  | -- | a peer connected-    BlockPeerConnect !Peer-  | -- | a peer disconnected-    BlockPeerDisconnect !Peer-  | -- | a peer sent a block-    BlockReceived !Peer !Block-  | -- | a peer could not find blocks-    BlockNotFound !Peer ![BlockHash]-  | -- | a peer sent a transaction-    TxRefReceived !Peer !Tx-  | -- | a peer announced transactions-    TxRefAvailable !Peer ![TxHash]-  | -- | clock tick for housekeeping; reply when done-    BlockPing !(Listen ())---- | Fatal errors of the block store.-data BlockException-  = -- | the header of a block in the database is not known to the node-    BlockNotInChain !BlockHash-  | -- | the database has no best block-    Uninitialized-  | -- | the database is inconsistent-    CorruptDatabase-  | -- | the node has no header at this height below this block-    AncestorNotInChain !BlockHeight !BlockHash-  | -- | importing mempool transactions failed-    MempoolImportFailed-  deriving (Show, Eq, Ord, Exception)---- | State of the sync against the syncing peer.-data Syncing = Syncing-  { -- | peer that blocks are downloaded from-    peer :: !Peer,-    -- | when it last delivered a block, or when syncing started-    time :: !UTCTime,-    -- | blocks requested and not yet imported, in chain order-    blocks :: ![BlockHash]-  }---- | Transaction received from a peer and waiting to be imported.-data PendingTx = PendingTx-  { -- | when it was received-    time :: !UTCTime,-    -- | the transaction-    tx :: !Tx,-    -- | parent transactions that are missing; empty if it can be imported-    deps :: !(HashSet TxHash)-  }-  deriving (Show, Eq, Ord)---- | Block store process state.-data BlockStore = BlockStore-  { -- | mailbox of the process-    mailbox :: !(Mailbox BlockStoreMessage),-    -- | configuration-    config :: !BlockStoreConfig,-    -- | syncing peer, if any-    peer :: !(TVar (Maybe Syncing)),-    -- | transactions received and waiting to be imported-    txs :: !(TVar (HashMap TxHash PendingTx)),-    -- | transactions requested from peers in the last twenty seconds-    requested :: !(TVar (HashSet TxHash)),-    -- | StatsD metrics, if enabled-    metrics :: !(Maybe StoreMetrics),-    -- | labels of the peers whose mempool was already requested-    syncedMempool :: !(TVar (HashSet Text))-  }---- | StatsD metrics of the block store.-data StoreMetrics = StoreMetrics-  { -- | height of the best block (@store.blocks@)-    blocks :: !StatGauge,-    -- | height of the best header (@store.headers@)-    headers :: !StatGauge,-    -- | transactions waiting to be imported (@store.queued_txs@)-    queuedTxs :: !StatGauge,-    -- | online peers (@store.peers@)-    peers :: !StatGauge,-    -- | transactions in the mempool (@store.mempool@)-    mempool :: !StatGauge,-    -- | milliseconds to import a block (@store.block@)-    block :: !StatTiming-  }---- | Register the block store metrics, if StatsD is configured, with their current values.-newStoreMetrics :: (MonadIO m) => BlockStoreConfig -> m (Maybe StoreMetrics)-newStoreMetrics cfg =-  forM cfg.stats $ \s -> liftIO $ do-    m <- withDB cfg.db getMempool-    b <--      fmap (maybe 0 (.height)) $-        withDB cfg.db $-          runMaybeT $-            MaybeT getBestBlock >>= MaybeT . getBlock-    h <- chainGetBest cfg.chain-    p <- getPeers cfg.peerMgr-    blocks <- g s "blocks" (fromIntegral b)-    headers <- g s "headers" (fromIntegral h.height)-    queuedTxs <- g s "queued_txs" 0-    peers <- g s "peers" (length p)-    mempool <- g s "mempool" (length m)-    block <- t s "block" 0-    return StoreMetrics {..}-  where-    g s x = newStatGauge s ("store." <> x)-    t s x = newStatTiming s ("store." <> x)---- | Update the gauge of the best block height.-setStoreHeight :: (MonadIO m) => BlockT m ()-setStoreHeight = void $ runMaybeT $ do-  m <- MaybeT (asks (.metrics))-  h <- MaybeT getBestBlock-  b <- MaybeT (getBlock h)-  setGauge m.blocks (fromIntegral b.height)---- | Update the gauge of the best header height.-setHeadersHeight :: (MonadIO m) => BlockT m ()-setHeadersHeight = void $ runMaybeT $ do-  m <- MaybeT (asks (.metrics))-  n <- liftIO . chainGetBest =<< asks (.config.chain)-  setGauge m.headers (fromIntegral n.height)---- | Update the gauge of transactions waiting to be imported.-setPendingTxs :: (MonadIO m) => BlockT m ()-setPendingTxs = void $ runMaybeT $ do-  m <- MaybeT (asks (.metrics))-  p <- readTVarIO =<< asks (.txs)-  setGauge m.queuedTxs (HashMap.size p)---- | Update the gauge of online peers.-setPeersConnected :: (MonadIO m) => BlockT m ()-setPeersConnected = void $ runMaybeT $ do-  m <- MaybeT (asks (.metrics))-  p <- liftIO . getPeers =<< asks (.config.peerMgr)-  setGauge m.peers (length p)---- | Record how long a successful block import took.-withBlockTiming :: (MonadIO m) => BlockT m (Either l r) -> BlockT m (Either l r)-withBlockTiming go = do-  asks (.metrics) >>= \case-    Just m -> do-      start <- round . (* 1000) <$> liftIO getPOSIXTime-      go >>= \case-        Right x -> do-          end <- round . (* 1000) <$> liftIO getPOSIXTime-          addTiming m.block (end - start)-          return (Right x)-        Left y -> return (Left y)-    Nothing -> go---- | Configuration for a block store.-data BlockStoreConfig = BlockStoreConfig-  { -- | secp256k1 context-    ctx :: !Ctx,-    -- | peer manager from running node-    peerMgr :: !PeerMgr,-    -- | chain from a running node-    chain :: !Chain,-    -- | listener for store events-    pub :: !(Publisher StoreEvent),-    -- | RocksDB database handle-    db :: !DatabaseReader,-    -- | network constants-    net :: !Network,-    -- | do not index new mempool transactions-    noMempool :: !Bool,-    -- | wipe mempool at start-    wipeMempool :: !Bool,-    -- | sync mempool from peers-    syncMempool :: !Bool,-    -- | blocks to request from the syncing peer in one batch-    blockWindow :: !Int,-    -- | delete transactions older than this number of days from mempool-    mempoolTimeout :: !Int,-    -- | disconnect syncing peer if inactive for this long-    peerTimeout :: !NominalDiffTime,-    -- | StatsD client, if metrics are enabled-    stats :: !(Maybe Stats),-    -- | log messages here-    logger :: !Logger-  }---- | Monad transformer of the block store process.-type BlockT m = ReaderT BlockStore m---- | Log a message with the logger of the block store.-logDebug, logInfo, logWarn, logError ::-  (MonadIO m, MonadReader BlockStore m) => Text -> m ()-logDebug = blockLog LevelDebug-logInfo = blockLog LevelInfo-logWarn = blockLog LevelWarn-logError = blockLog LevelError---- | Log a message at a level with the logger of the block store.-blockLog :: (MonadIO m, MonadReader BlockStore m) => LogLevel -> Text -> m ()-blockLog l msg = asks (.config.logger) >>= \lg -> logAt l lg msg---- | Run an import against the database, returning the import error if it fails.-runImport ::-  (MonadIO m) =>-  Network ->-  Ctx ->-  ImportM a ->-  BlockT m (Either ImportException a)-runImport net ctx f = fmap fst <$> runImportMempool net ctx f---- | Like 'runImport', but also return how the import changed the mempool.--- The mempool gauge is kept up to date from those changes instead of--- counting the whole mempool on every clock tick.-runImportMempool ::-  (MonadIO m) =>-  Network ->-  Ctx ->-  ImportM a ->-  BlockT m (Either ImportException (a, MempoolChanges))-runImportMempool net ctx f = do-  r <- ReaderT $ \r ->-    liftIO . try $ runWriterMempool net ctx r.config.db r.config.logger f-  forM_ r $ \(_, c) -> asks (.metrics) >>= mapM_ (\m -> do-    incrementGauge m.mempool (length c.added)-    decrementGauge m.mempool (length c.removed))-  return r---- | Run an import and publish the transactions that it added to the--- mempool or removed from it, then the new best block if there is one.--- Transactions of the block are not published.-runImportNotify ::-  (MonadIO m) =>-  Maybe Block ->-  ImportM a ->-  BlockT m (Either ImportException a)-runImportNotify block f = do-  net <- getNetwork-  ctx <- getCtx-  runImportMempool net ctx f >>= \case-    Left e -> return (Left e)-    Right (x, c) -> do-      l <- asks (.config.pub)-      forM_ (filter notInBlock c.removed) $ \h ->-        publish (StoreMempoolDelete h) l-      forM_ (filter notInBlock c.added) $ \h ->-        publish (StoreMempoolNew h) l-      forM_ block $ \b ->-        publish (StoreBestBlock (headerHash b.header)) l-      return (Right x)-  where-    inBlock = maybe HashSet.empty (HashSet.fromList . map txHash . (.txs)) block-    notInBlock = not . (`HashSet.member` inBlock)---- | Run a database query from the block store.-runRocksDB :: ReaderT DatabaseReader m a -> BlockT m a-runRocksDB f =-  ReaderT $ runReaderT f . (.config.db)--instance (MonadIO m) => StoreReadBase (BlockT m) where-  getCtx =-    asks (.config.ctx)-  getNetwork =-    runRocksDB getNetwork-  getBestBlock =-    runRocksDB getBestBlock-  getBlocksAtHeight =-    runRocksDB . getBlocksAtHeight-  getBlock =-    runRocksDB . getBlock-  getTxData =-    runRocksDB . getTxData-  getManyTxData =-    runRocksDB . getManyTxData-  getSpender =-    runRocksDB . getSpender-  getUnspent =-    runRocksDB . getUnspent-  getBalance =-    runRocksDB . getBalance-  getMempool =-    runRocksDB getMempool-  getMempoolPage =-    runRocksDB . getMempoolPage--instance (MonadUnliftIO m) => StoreReadExtra (BlockT m) where-  getMaxGap =-    runRocksDB getMaxGap-  getInitialGap =-    runRocksDB getInitialGap-  getAddressesTxs as =-    runRocksDB . getAddressesTxs as-  getAddressesUnspents as =-    runRocksDB . getAddressesUnspents as-  getAddressUnspents a =-    runRocksDB . getAddressUnspents a-  getAddressTxs a =-    runRocksDB . getAddressTxs a-  getNumTxData =-    runRocksDB . getNumTxData-  getBalances =-    runRocksDB . getBalances-  xPubBals =-    runRocksDB . xPubBals-  xPubUnspents x l =-    runRocksDB . xPubUnspents x l-  xPubTxs x l =-    runRocksDB . xPubTxs x l-  xPubTxCount x =-    runRocksDB . xPubTxCount x---- | Run block store process.-withBlockStore ::-  (MonadUnliftIO m) =>-  BlockStoreConfig ->-  (BlockStore -> m a) ->-  m a-withBlockStore cfg action = do-  pb <- newTVarIO Nothing-  ts <- newTVarIO HashMap.empty-  rq <- newTVarIO HashSet.empty-  sy <- newTVarIO HashSet.empty-  inbox <- newInbox-  metrics <- newStoreMetrics cfg-  let r =-        BlockStore-          { mailbox = inboxToMailbox inbox,-            config = cfg,-            peer = pb,-            txs = ts,-            requested = rq,-            metrics = metrics,-            syncedMempool = sy-          }-  withAsync (runReaderT (go inbox) r) $ \a -> do-    link a-    action r-  where-    go inbox = do-      ini-      wipe-      run inbox-    del txs = do-      logAt LevelInfo cfg.logger $-        "Deleting " <> cs (show (length txs)) <> " transactions"-      forM_ txs $ \(_, th) -> deleteUnconfirmedTx False th-    wipe_it txs = do-      net <- getNetwork-      ctx <- getCtx-      let (txs1, txs2) = splitAt 1000 txs-      unless (null txs1) $-        runImport net ctx (del txs1) >>= \case-          Left e -> do-            logError $-              "Could not wipe mempool: " <> cs (show e)-            throwIO e-          Right () -> wipe_it txs2-    wipe-      | cfg.wipeMempool =-          getMempool >>= wipe_it-      | otherwise =-          return ()-    ini = do-      net <- getNetwork-      ctx <- getCtx-      runImport net ctx initBest >>= \case-        Left e -> do-          logError $-            "Could not initialize: " <> cs (show e)-          throwIO e-        Right () -> return ()-    run inbox =-      withAsync (pingMe (inboxToMailbox inbox)) $ \a ->-        link a >> runBlockStoreLoop inbox---- | Handle messages forever.-runBlockStoreLoop ::-  (InChan mbox, MonadUnliftIO m) =>-  mbox BlockStoreMessage ->-  ReaderT BlockStore m b-runBlockStoreLoop inbox =-  loopForever $ do-    logDebug "Waiting for new event..."-    msg <- receive inbox-    ReaderT $ runReaderT $ processBlockStoreMessage msg---- | Whether the best block in the database is the best header of the node. If--- so, release the syncing peer.-isInSync :: (MonadIO m) => BlockT m Bool-isInSync =-  getBestBlock >>= \case-    Nothing -> do-      logError "Block database uninitialized"-      throwIO Uninitialized-    Just bb -> do-      cb <- asks (.config.chain) >>= liftIO . chainGetBest-      if headerHash cb.header == bb-        then clearSyncingState >> return True-        else return False---- | Run an action only if the database is in sync.-guardSync :: (MonadIO m) => BlockT m () -> BlockT m ()-guardSync f = isInSync >>= \s -> when s f---- | Run an action only if the mempool is indexed.-guardMempool :: (Monad m) => BlockT m () -> BlockT m ()-guardMempool f = asks (.config.noMempool) >>= \n -> unless n f---- | Ask a peer for its mempool, once per peer, if in sync and mempool sync is enabled.-syncMempool :: (MonadUnliftIO m) => Peer -> BlockT m ()-syncMempool p =-  guardMempool . guardSync $ do-    s <- asks (.config.syncMempool)-    m <- asks (.syncedMempool)-    y <- not . HashSet.member p.label <$> readTVarIO m-    when (s && y) $ do-      logDebug $-        "Requesting mempool from peer: " <> p.label-      atomically . modifyTVar m $ HashSet.insert p.label-      liftIO $ MMempool `sendMessage` p---- | Import a block from the syncing peer, then ask for more blocks or, if now in--- sync, sync the mempool. Disconnects the peer if it is not the syncing peer, if--- the block is unknown to the node, or if the block cannot be imported.-processBlock ::-  (MonadUnliftIO m) =>-  Peer ->-  Block ->-  BlockT m ()-processBlock peer block = void . runMaybeT $ do-  checkPeer peer >>= \case-    True -> return ()-    False -> do-      logError $-        "Non-syncing peer "-          <> peer.label-          <> " sent block "-          <> blockHashToHex blockhash-      killPeer peer-      mzero-  node <--    getBlockNode blockhash >>= \case-      Just b -> return b-      Nothing -> do-        logError $-          "Peer "-            <> peer.label-            <> " sent unknown block "-            <> blockHashToHex blockhash-        killPeer peer-        mzero-  logDebug $-    "Processing block "-      <> blockText node Nothing-      <> " from peer "-      <> peer.label-  lift $ do-    ret <- withBlockTiming $ runImportNotify (Just block) (importBlock block node)-    case ret of-      Left e -> failure e-      Right _ -> success node-  where-    header = block.header-    blockhash = headerHash header-    hexhash = blockHashToHex blockhash-    success node = do-      logInfo $-        "Best block: " <> blockText node (Just block)-      removeSyncingBlock $ headerHash node.header-      touchPeer-      isInSync >>= \case-        False -> syncMe-        True -> do-          updateOrphans-          syncMempool peer-    failure e = do-      logError $-        "Error importing block "-          <> hexhash-          <> " from peer: "-          <> peer.label-          <> ": "-          <> cs (show e)-      killPeer peer---- | Set the blocks requested from the syncing peer.-setSyncingBlocks ::-  (MonadReader BlockStore m, MonadIO m) =>-  [BlockHash] ->-  m ()-setSyncingBlocks hs =-  asks (.peer) >>= \box ->-    atomically $-      modifyTVar box $ \case-        Nothing -> Nothing-        Just x -> Just (x :: Syncing) {blocks = hs}---- | Blocks requested from the syncing peer and not yet imported.-getSyncingBlocks :: (MonadReader BlockStore m, MonadIO m) => m [BlockHash]-getSyncingBlocks =-  asks (.peer) >>= readTVarIO >>= \case-    Nothing -> return []-    Just x -> return x.blocks---- | Add blocks to those requested from the syncing peer.-addSyncingBlocks ::-  (MonadReader BlockStore m, MonadIO m) =>-  [BlockHash] ->-  m ()-addSyncingBlocks hs =-  asks (.peer) >>= \box ->-    atomically $-      modifyTVar box $ \case-        Nothing -> Nothing-        Just x -> Just (x :: Syncing) {blocks = x.blocks <> hs}---- | Remove a block from those requested from the syncing peer.-removeSyncingBlock ::-  (MonadReader BlockStore m, MonadIO m) =>-  BlockHash ->-  m ()-removeSyncingBlock h = do-  box <- asks (.peer)-  atomically $-    modifyTVar box $ \case-      Nothing -> Nothing-      Just x -> Just (x :: Syncing) {blocks = delete h x.blocks}---- | Whether a peer is the syncing peer.-checkPeer :: (MonadIO m, MonadReader BlockStore m) => Peer -> m Bool-checkPeer p = fmap (fromMaybe False) <$> runMaybeT $ do-  Syncing {peer} <- MaybeT getSyncingState-  return $ peer == p---- | Header of a block from the node.-getBlockNode ::-  (MonadIO m, MonadReader BlockStore m) =>-  BlockHash ->-  m (Maybe BlockNode)-getBlockNode blockhash = do-  ch <- asks (.config.chain)-  liftIO $ chainGetBlock ch blockhash---- | Disconnect a peer that could not find requested blocks.-processNoBlocks ::-  (MonadIO m) =>-  Peer ->-  [BlockHash] ->-  BlockT m ()-processNoBlocks p hs = do-  forM_ (zip [(1 :: Int) ..] hs) $ \(i, h) ->-    logError $-      "Block "-        <> cs (show i)-        <> "/"-        <> cs (show (length hs))-        <> " "-        <> blockHashToHex h-        <> " not found by peer: "-        <> p.label-  killPeer p---- | Queue a transaction received from a peer for import.-processTx :: (MonadIO m) => Peer -> Tx -> BlockT m ()-processTx p tx = guardMempool $ do-  t <- liftIO getCurrentTime-  logDebug $-    "Received tx "-      <> txHashToHex (txHash tx)-      <> " by peer: "-      <> p.label-  addPendingTx $ PendingTx t tx HashSet.empty---- | Drop queued transactions received more than an hour ago.-pruneOrphans :: (MonadIO m) => BlockT m ()-pruneOrphans = guardMempool $ do-  ts <- asks (.txs)-  now <- liftIO getCurrentTime-  -- keep pending transactions received within the last hour, drop older ones-  atomically . modifyTVar' ts . HashMap.filter $ \p ->-    now `diffUTCTime` p.time <= 3600---- | Queue a transaction for import.-addPendingTx :: (MonadIO m) => PendingTx -> BlockT m ()-addPendingTx p = do-  ts <- asks (.txs)-  rq <- asks (.requested)-  atomically $ do-    modifyTVar ts $ HashMap.insert th p-    modifyTVar rq $ HashSet.delete th-  setPendingTxs-  where-    th = txHash p.tx---- | Remember for twenty seconds that a transaction was requested, so that it is not requested again.-addRequestedTx :: (MonadIO m) => TxHash -> BlockT m ()-addRequestedTx th = do-  qbox <- asks (.requested)-  atomically $ modifyTVar qbox $ HashSet.insert th-  liftIO . void . async $ do-    threadDelay 20000000-    atomically $ modifyTVar qbox $ HashSet.delete th---- | Whether a transaction is queued or was requested recently.-isPending :: (MonadIO m) => TxHash -> BlockT m Bool-isPending th = do-  tbox <- asks (.txs)-  qbox <- asks (.requested)-  atomically $ do-    ts <- readTVar tbox-    rs <- readTVar qbox-    return $-      th `HashMap.member` ts-        || th `HashSet.member` rs---- | Take up to this many queued transactions that have no missing parents, in--- dependency order, out of the queue.-pendingTxs :: (MonadIO m) => Int -> BlockT m [PendingTx]-pendingTxs i = do-  selected <--    asks (.txs) >>= \box -> atomically $ do-      pending <- readTVar box-      let (selected, rest) = select pending-      writeTVar box rest-      return selected-  setPendingTxs-  return selected-  where-    select pend =-      let eligible = HashMap.filter (null . (.deps)) pend-          orphans = HashMap.difference pend eligible-          selected = take i $ sortit eligible-          remaining = HashMap.filter (`notElem` selected) eligible-       in (selected, remaining <> orphans)-    sortit m =-      let sorted = sortTxs $ map (.tx) $ HashMap.elems m-          txids = map (txHash . snd) sorted-       in mapMaybe (`HashMap.lookup` m) txids---- | Mark a transaction as no longer missing for the queued transactions that spend it.-fulfillOrphans :: (MonadIO m) => BlockStore -> TxHash -> m ()-fulfillOrphans block_read th =-  atomically $ modifyTVar box (HashMap.map fulfill)-  where-    box = block_read.txs-    fulfill p = p {deps = HashSet.delete th p.deps}---- | Drop queued transactions that are already in the database, and recompute the--- missing parents of the others.-updateOrphans ::-  ( StoreReadBase m,-    MonadIO m,-    MonadReader BlockStore m-  ) =>-  m ()-updateOrphans = do-  box <- asks (.txs)-  pending <- readTVarIO box-  let orphans = HashMap.filter (not . null . (.deps)) pending-  updated <- forM orphans $ \p -> do-    let tx = p.tx-    exists (txHash tx) >>= \case-      True -> return Nothing-      False -> Just <$> fill_deps p-  let pruned = HashMap.map fromJust $ HashMap.filter isJust updated-  atomically $ writeTVar box pruned-  where-    exists th =-      getTxData th >>= \case-        Nothing -> return False-        Just TxData {deleted = True} -> return False-        Just TxData {deleted = False} -> return True-    prev_utxos tx = catMaybes <$> mapM (getUnspent . (.outpoint)) tx.inputs-    fulfill p unspent =-      let unspent_hash = unspent.outpoint.hash-          new_deps = HashSet.delete unspent_hash p.deps-       in p {deps = new_deps}-    fill_deps p = do-      let tx = p.tx-      unspents <- prev_utxos tx-      return $ foldl fulfill p unspents---- | Queue a transaction that spends unknown outputs, recording which parents are missing.-newOrphanTx ::-  (MonadIO m) =>-  BlockStore ->-  UTCTime ->-  Tx ->-  WriterT m ()-newOrphanTx block_read time tx = do-  logAt LevelDebug block_read.config.logger $-    "Import tx "-      <> txHashToHex (txHash tx)-      <> ": Orphan"-  let box = block_read.txs-  unspents <- catMaybes <$> mapM getUnspent prevs-  let unspent_set = HashSet.fromList (map (.outpoint) unspents)-      missing_set = HashSet.difference prev_set unspent_set-      missing_txs = HashSet.map (.hash) missing_set-  atomically . modifyTVar box $-    HashMap.insert-      (txHash tx)-      PendingTx-        { time = time,-          tx = tx,-          deps = missing_txs-        }-  where-    prev_set = HashSet.fromList prevs-    prevs = map (.outpoint) tx.inputs---- | Import a transaction into the mempool. Returns 'True' if imported. A--- transaction that spends unknown outputs is queued as an orphan; other errors--- are ignored.-importMempoolTx :: BlockStore -> UTCTime -> Tx -> ImportM Bool-importMempoolTx block_read time tx =-  catch new_mempool_tx handle_error-  where-    tx_hash = txHash tx-    handle_error :: ImportException -> ImportM Bool-    handle_error Orphan = do-      newOrphanTx block_read time tx-      return False-    handle_error _ = return False-    seconds = floor (utcTimeToPOSIXSeconds time)-    new_mempool_tx = do-      t <- newMempoolTx tx seconds-      logAt LevelInfo block_read.config.logger $-        bool "Already have" "Imported" t-          <> " tx "-          <> txHashToHex (txHash tx)-      when t $ fulfillOrphans block_read tx_hash-      return t---- | Import up to 2000 queued transactions.-processMempool :: (MonadIO m) => BlockT m ()-processMempool = guardMempool $ do-  txs <- pendingTxs 2000-  block_read <- ask-  unless (null txs) $ import_txs block_read txs-  where-    run_import block_read p =-      importMempoolTx block_read p.time p.tx-    import_txs block_read txs =-      runImportNotify Nothing (mapM (run_import block_read) txs) >>= \case-        Left e -> report_error e-        Right _ -> return ()-    report_error e = do-      logError $-        "Error processing mempool: " <> cs (show e)-      throwIO e---- | Delete up to 1000 mempool transactions older than the configured number of days.-pruneMempool :: (MonadIO m) => BlockT m ()-pruneMempool =-  guardMempool $ do-    days <- asks (.config.mempoolTimeout)-    when (days > 0) $ do-      time <- (floor . utcTimeToPOSIXSeconds) <$> liftIO getCurrentTime-      let thresh = time - (fromIntegral days * 24 * 60 * 60)-      -- runs on every clock tick: read only the expired entries, oldest-      -- first, from the index by time-      txs <- map snd <$> runRocksDB (getMempoolBefore thresh 1000)-      unless (null txs) $ do-        logInfo $-          "Deleting " <> cs (show (length txs)) <> " old transactions from mempool"-        runImportNotify Nothing (mapM_ (deleteUnconfirmedTx False) txs) >>= \case-          Left e ->-            logError $-              "Error pruning mempool: " <> cs (show e)-          Right () -> return ()---- | Request from a peer the announced transactions that are neither queued,--- recently requested nor in the database. Only while in sync.-processTxs ::-  (MonadIO m) =>-  Peer ->-  [TxHash] ->-  BlockT m ()-processTxs p hs = guardMempool . guardSync $ do-  logDebug $-    "Received inventory with "-      <> cs (show (length hs))-      <> " transactions from peer: "-      <> p.label-  xs <- catMaybes <$> zip_counter process_tx-  unless (null xs) $ go xs-  where-    len = length hs-    zip_counter = forM (zip [(1 :: Int) ..] hs) . uncurry-    process_tx i h =-      isPending h >>= \case-        True -> do-          logDebug $-            "Tx "-              <> cs (show i)-              <> "/"-              <> cs (show len)-              <> " "-              <> txHashToHex h-              <> ": "-              <> "Already Pending"-          return Nothing-        False ->-          getTxData h >>= \case-            Just _ -> do-              logDebug $-                "Tx "-                  <> cs (show i)-                  <> "/"-                  <> cs (show len)-                  <> " "-                  <> txHashToHex h-                  <> ": "-                  <> "Already Imported"-              return Nothing-            Nothing -> do-              logDebug $-                "Tx "-                  <> cs (show i)-                  <> "/"-                  <> cs (show len)-                  <> " "-                  <> txHashToHex h-                  <> ": "-                  <> "Requesting"-              return (Just h)-    go xs = do-      mapM_ addRequestedTx xs-      net <- asks (.config.net)-      let inv = if net.segWit then InvWitnessTx else InvTx-          vec = map (InvVector inv . (.get)) xs-          msg = MGetData (GetData vec)-      liftIO $ msg `sendMessage` p---- | Record that the syncing peer just delivered something.-touchPeer ::-  ( MonadIO m,-    MonadReader BlockStore m-  ) =>-  m ()-touchPeer =-  getSyncingState >>= \case-    Nothing -> return ()-    Just _ -> do-      box <- asks (.peer)-      now <- liftIO getCurrentTime-      atomically $-        modifyTVar box $-          fmap $-            \x -> (x :: Syncing) {time = now}---- | Disconnect the syncing peer if it has been silent for too long.-checkTime :: (MonadIO m) => BlockT m ()-checkTime =-  asks (.peer) >>= readTVarIO >>= \case-    Nothing -> return ()-    Just-      Syncing-        { time = t,-          peer = p-        } -> do-        now <- liftIO getCurrentTime-        peer_time_out <- asks (.config.peerTimeout)-        when (now `diffUTCTime` t > peer_time_out) $ do-          logError $-            "Syncing peer timeout: " <> p.label-          killPeer p---- | Revert blocks until the best block in the database is in the main chain of headers.-revertToMainChain :: (MonadIO m) => BlockT m ()-revertToMainChain = do-  h <- headerHash . (.header) <$> getBest-  ch <- asks (.config.chain)-  net <- getNetwork-  ctx <- getCtx-  liftIO (chainBlockMain ch h) >>= \x -> unless x $ do-    logWarn $-      "Reverting best block: "-        <> blockHashToHex h-    runImport net ctx (revertBlock h) >>= \case-      Left e -> do-        logError $-          "Could not revert block "-            <> blockHashToHex h-            <> ": "-            <> cs (show e)-        throwIO e-      Right () -> setSyncingBlocks []-    revertToMainChain---- | Header of the best block in the database.-getBest :: (MonadIO m) => BlockT m BlockNode-getBest = do-  bb <--    getBestBlock >>= \case-      Just b -> return b-      Nothing -> do-        logError "No best block set"-        throwIO Uninitialized-  ch <- asks (.config.chain)-  liftIO (chainGetBlock ch bb) >>= \case-    Just x -> return x-    Nothing -> do-      logError $-        "Header not found for best block: "-          <> blockHashToHex bb-      throwIO (BlockNotInChain bb)---- | Header of the last block requested from the syncing peer, or of the best block if none.-getSyncBest :: (MonadIO m) => BlockT m BlockNode-getSyncBest = do-  bb <--    getSyncingBlocks >>= \case-      [] ->-        getBestBlock >>= \case-          Just b -> return b-          Nothing -> do-            logError "No best block set"-            throwIO Uninitialized-      hs -> return $ last hs-  ch <- asks (.config.chain)-  liftIO (chainGetBlock ch bb) >>= \case-    Just x -> return x-    Nothing -> do-      logError $-        "Header not found for block: "-          <> blockHashToHex bb-      throwIO (BlockNotInChain bb)---- | The syncing peer, when it is time to ask it for the next batch of blocks.-shouldSync :: (MonadIO m) => BlockT m (Maybe Peer)-shouldSync =-  isInSync >>= \case-    True -> return Nothing-    False ->-      getSyncingState >>= \case-        Nothing -> return Nothing-        Just Syncing {peer = p, blocks = bs} -> do-          window <- blockWindow-          return $-            if needsMoreBlocks window (length bs)-              then Just p-              else Nothing---- | Blocks to request from the syncing peer in one batch. Never below one,--- or the store would never ask for any block.-blockWindow :: (Monad m) => BlockT m Int-blockWindow = asks (max 1 . (.config.blockWindow))---- | Whether to ask for the next batch, given the window size and how many--- requested blocks are still waiting to be imported. Asking once fewer than--- a quarter of the window remain keeps the peer busy, while the queue of--- downloaded blocks never grows much beyond one window. The threshold is at--- least one, so that a window smaller than four still refills once empty.-needsMoreBlocks :: Int -> Int -> Bool-needsMoreBlocks window outstanding =-  outstanding < max 1 (window `div` 4)---- | Ask the syncing peer for the next batch of blocks, if the window has room.-syncMe :: (MonadIO m) => BlockT m ()-syncMe = do-  revertToMainChain-  shouldSync >>= \case-    Nothing -> return ()-    Just p -> do-      bb <- getSyncBest-      bh <- getbh-      when (bb /= bh) $ do-        bns <- sel bb bh-        iv <- getiv bns-        logDebug $-          "Requesting "-            <> fromString (show (length iv))-            <> " blocks from peer: "-            <> p.label-        addSyncingBlocks $ map (headerHash . (.header)) bns-        liftIO $ MGetData (GetData iv) `sendMessage` p-  where-    getiv bns = do-      w <- asks (.config.net.segWit)-      let i = if w then InvWitnessBlock else InvBlock-          f = InvVector i . (.get) . headerHash . (.header)-      return $ map f bns-    getbh =-      liftIO . chainGetBest =<< asks (.config.chain)-    -- The blocks right after the last requested one, up to one window or-    -- the chain tip, whichever comes first.-    sel bb bh = do-      window <- blockWindow-      top <- findAncestor (min (bb.height + fromIntegral window) bh.height) bh-      ch <- asks (.config.chain)-      ps <- liftIO $ chainGetParents ch (bb.height + 1) top-      return $ ps <> [top]---- | Ancestor of a header at a height. Throws 'AncestorNotInChain' if missing.-findAncestor ::-  (MonadIO m, MonadReader BlockStore m) =>-  BlockHeight ->-  BlockNode ->-  m BlockNode-findAncestor height target = do-  ch <- asks (.config.chain)-  liftIO (chainGetAncestor ch height target) >>= \case-    Just ancestor -> return ancestor-    Nothing -> do-      let h = headerHash target.header-      logError $-        "Could not find header for ancestor of block "-          <> blockHashToHex h-          <> " at height "-          <> cs (show target.height)-      throwIO $ AncestorNotInChain height h---- | Forget a peer: release it if it was the syncing peer, and forget that its mempool was requested.-finishPeer ::-  (MonadIO m, MonadReader BlockStore m) =>-  Peer ->-  m ()-finishPeer p = do-  box <- asks (.peer)-  readTVarIO box >>= \case-    Just Syncing {peer = p'} | p == p' -> reset_it box-    _ -> return ()-  y <- asks (.syncedMempool)-  atomically $ modifyTVar y $ HashSet.delete p.label-  where-    reset_it box = do-      atomically $ writeTVar box Nothing-      logDebug $ "Releasing peer: " <> p.label-      liftIO $ setFree p---- | Make a peer the syncing peer if there is none and the peer is not busy.-trySetPeer :: (MonadIO m) => Peer -> BlockT m Bool-trySetPeer p =-  getSyncingState >>= \case-    Just _ -> return False-    Nothing -> set_it-  where-    set_it =-      liftIO (setBusy p) >>= \case-        False -> return False-        True -> do-          logDebug $-            "Locked peer: " <> p.label-          box <- asks (.peer)-          now <- liftIO getCurrentTime-          atomically . writeTVar box $-            Just-              Syncing-                { peer = p,-                  time = now,-                  blocks = []-                }-          return True---- | If not in sync and there is no syncing peer, pick the fastest free peer and start syncing.-trySyncing :: (MonadIO m) => BlockT m ()-trySyncing =-  isInSync >>= \case-    True -> return ()-    False ->-      getSyncingState >>= \case-        Just _ -> return ()-        Nothing -> online_peer-  where-    recurse [] = return ()-    recurse (p : ps) =-      trySetPeer p >>= \case-        False -> recurse ps-        True -> syncMe-    online_peer = do-      ops <- liftIO . getPeers =<< asks (.config.peerMgr)-      let ps = map (.mailbox) ops-      recurse ps---- | Start syncing against a newly connected peer if needed, or request its mempool if in sync.-trySyncingPeer :: (MonadUnliftIO m) => Peer -> BlockT m ()-trySyncingPeer p =-  isInSync >>= \case-    True -> syncMempool p-    False ->-      trySetPeer p >>= \case-        False -> return ()-        True -> syncMe---- | State of the sync against the syncing peer, if any.-getSyncingState ::-  (MonadIO m, MonadReader BlockStore m) => m (Maybe Syncing)-getSyncingState =-  readTVarIO =<< asks (.peer)---- | Release the syncing peer, if any.-clearSyncingState ::-  (MonadIO m, MonadReader BlockStore m) => m ()-clearSyncingState =-  asks (.peer) >>= readTVarIO >>= \case-    Nothing -> return ()-    Just Syncing {peer = p} -> finishPeer p---- | Handle a message sent to the block store.-processBlockStoreMessage ::-  (MonadUnliftIO m) =>-  BlockStoreMessage ->-  BlockT m ()-processBlockStoreMessage (BlockNewBest node) = do-  logDebug $-    "New best block mined at height "-      <> cs (show node.height)-      <> ": "-      <> blockHashToHex (headerHash node.header)-  trySyncing-processBlockStoreMessage (BlockPeerConnect p) = do-  logDebug $-    "New peer connected: " <> p.label-  trySyncingPeer p-processBlockStoreMessage (BlockPeerDisconnect p) = do-  logDebug $-    "Peer disconnected: " <> p.label-  finishPeer p-processBlockStoreMessage (BlockReceived p b) = do-  logDebug $-    "Received block: "-      <> blockHashToHex (headerHash b.header)-  processBlock p b-processBlockStoreMessage (BlockNotFound p bs) = do-  logDebug $-    "Blocks not found by peer "-      <> p.label-      <> ": "-      <> T.unwords (map blockHashToHex bs)-  processNoBlocks p bs-processBlockStoreMessage (TxRefReceived p tx) = do-  logDebug $-    "Transaction received from peer "-      <> p.label-      <> ": "-      <> txHashToHex (txHash tx)-  processTx p tx-processBlockStoreMessage (TxRefAvailable p ts) = do-  logDebug $-    "Transactions available from peer "-      <> p.label-      <> ": "-      <> T.unwords (map txHashToHex ts)-  processTxs p ts-processBlockStoreMessage (BlockPing r) = do-  logDebug "Internal clock event"-  setStoreHeight-  setHeadersHeight-  setPendingTxs-  setPeersConnected-  trySyncing-  processMempool-  pruneOrphans-  pruneMempool-  checkTime-  atomically (r ())---- | Send a clock tick to the block store every 0.1 to 1 second, waiting for each to be handled.-pingMe :: (MonadIO m) => Mailbox BlockStoreMessage -> m ()-pingMe mbox =-  loopForever $ do-    BlockPing `query` mbox-    delay <- liftIO $ randomRIO (100 * 1000, 1000 * 1000)-    threadDelay delay---- | Tell the block store that a peer connected.-blockStorePeerConnect :: (MonadIO m) => Peer -> BlockStore -> m ()-blockStorePeerConnect peer store =-  BlockPeerConnect peer `send` store.mailbox---- | Tell the block store that a peer disconnected.-blockStorePeerDisconnect ::-  (MonadIO m) => Peer -> BlockStore -> m ()-blockStorePeerDisconnect peer store =-  BlockPeerDisconnect peer `send` store.mailbox---- | Tell the block store that the node has a new best header.-blockStoreHead ::-  (MonadIO m) => BlockNode -> BlockStore -> m ()-blockStoreHead node store =-  BlockNewBest node `send` store.mailbox---- | Give the block store a block received from a peer.-blockStoreBlock ::-  (MonadIO m) => Peer -> Block -> BlockStore -> m ()-blockStoreBlock peer block store =-  BlockReceived peer block `send` store.mailbox---- | Tell the block store that a peer could not find blocks.-blockStoreNotFound ::-  (MonadIO m) => Peer -> [BlockHash] -> BlockStore -> m ()-blockStoreNotFound peer blocks store =-  BlockNotFound peer blocks `send` store.mailbox---- | Give the block store a transaction received from a peer.-blockStoreTx ::-  (MonadIO m) => Peer -> Tx -> BlockStore -> m ()-blockStoreTx peer tx store =-  TxRefReceived peer tx `send` store.mailbox---- | Tell the block store that a peer announced transactions.-blockStoreTxHash ::-  (MonadIO m) => Peer -> [TxHash] -> BlockStore -> m ()-blockStoreTxHash peer txhashes store =-  TxRefAvailable peer txhashes `send` store.mailbox---- | 'STM' version of 'blockStorePeerConnect'.-blockStorePeerConnectSTM ::-  Peer -> BlockStore -> STM ()-blockStorePeerConnectSTM peer store =-  BlockPeerConnect peer `sendSTM` store.mailbox---- | 'STM' version of 'blockStorePeerDisconnect'.-blockStorePeerDisconnectSTM ::-  Peer -> BlockStore -> STM ()-blockStorePeerDisconnectSTM peer store =-  BlockPeerDisconnect peer `sendSTM` store.mailbox---- | 'STM' version of 'blockStoreHead'.-blockStoreHeadSTM ::-  BlockNode -> BlockStore -> STM ()-blockStoreHeadSTM node store =-  BlockNewBest node `sendSTM` store.mailbox---- | 'STM' version of 'blockStoreBlock'.-blockStoreBlockSTM ::-  Peer -> Block -> BlockStore -> STM ()-blockStoreBlockSTM peer block store =-  BlockReceived peer block `sendSTM` store.mailbox---- | 'STM' version of 'blockStoreNotFound'.-blockStoreNotFoundSTM ::-  Peer -> [BlockHash] -> BlockStore -> STM ()-blockStoreNotFoundSTM peer blocks store =-  BlockNotFound peer blocks `sendSTM` store.mailbox---- | 'STM' version of 'blockStoreTx'.-blockStoreTxSTM ::-  Peer -> Tx -> BlockStore -> STM ()-blockStoreTxSTM peer tx store =-  TxRefReceived peer tx `sendSTM` store.mailbox---- | 'STM' version of 'blockStoreTxHash'.-blockStoreTxHashSTM ::-  Peer -> [TxHash] -> BlockStore -> STM ()-blockStoreTxHashSTM peer txhashes store =-  TxRefAvailable peer txhashes `sendSTM` store.mailbox---- | Number of transactions queued or requested by the block store.-blockStorePendingTxs ::-  (MonadIO m) => BlockStore -> m Int-blockStorePendingTxs =-  atomically . blockStorePendingTxsSTM---- | 'STM' version of 'blockStorePendingTxs'.-blockStorePendingTxsSTM ::-  BlockStore -> STM Int-blockStorePendingTxsSTM BlockStore {..} = do-  x <- HashMap.keysSet <$> readTVar txs-  y <- readTVar requested-  return $ HashSet.size $ x `HashSet.union` y+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}++-- |+-- Module      : Haskoin.Store.BlockStore+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The block store process downloads blocks and transactions from peers and+-- imports them into the database.+--+-- = Blocks+--+-- When the best header of the node is ahead of the best block in the database,+-- the block store picks one online peer as its syncing peer and asks it for+-- blocks in batches of 'blockWindow'. Each block is imported as soon as it+-- arrives. If the best block of the database is no longer in the main chain of+-- headers, blocks are reverted until it is. A syncing peer that sends a block+-- that was not requested, cannot find a block, or stays silent for longer than+-- 'peerTimeout' is disconnected. The state of the download, and every rule+-- about it, is the pure state machine of "Haskoin.Store.BlockStore.Sync";+-- this module carries out what its transitions decide.+--+-- = Mempool+--+-- Once the database is in sync, the block store asks each peer for its mempool+-- (if 'syncMempool' is set) and requests every transaction that peers+-- announce. Received transactions are queued and imported in batches on every+-- clock tick, in dependency order. Transactions that spend unknown outputs are+-- kept for up to an hour, waiting for their parents. Mempool transactions older+-- than 'mempoolTimeout' days are deleted. The transactions on their way in,+-- requested or received, are the pure value of+-- "Haskoin.Store.BlockStore.Pending".+--+-- = Feeding the block store+--+-- The block store does not listen to the node by itself.+-- "Haskoin.Store.Manager" forwards node events to it with the+-- @blockStore*@ functions of this module.+-- = Why it works this way+--+-- * __Blocks come from one peer at a time.__ Blocks must be imported in order,+--   so downloading from several peers at once would only buffer blocks that cannot+--   be imported yet. One syncing peer, kept busy with a sliding window of+--   requests, uses the network well without unbounded buffering.+--+-- * __The window refills early.__ The next batch is requested when fewer than a+--   quarter of the window are still outstanding, so the peer never sits idle+--   while the store imports, and the backlog never grows much beyond one window.+--+-- * __Mempool sync waits for the tip.__ Unconfirmed transactions only make sense+--   against the current chain, so they are requested once the database is in+--   sync.+--+-- * __Orphans get a grace period.__ A transaction can arrive before its parent.+--   It is held for up to an hour, waiting for the parent, instead of being+--   dropped and requested again.+--+-- * __Imports are batched.__ Mempool transactions are imported up to 2000 at a+--   time on each clock tick, which amortises the cost of a database write over+--   many transactions during bursts.+module Haskoin.Store.BlockStore+  ( -- * Block Store+    BlockStore,+    BlockStoreConfig (..),+    withBlockStore,+    blockStorePeerConnect,+    blockStorePeerConnectSTM,+    blockStorePeerDisconnect,+    blockStorePeerDisconnectSTM,+    blockStoreHead,+    blockStoreHeadSTM,+    blockStoreBlock,+    blockStoreBlockSTM,+    blockStoreNotFound,+    blockStoreNotFoundSTM,+    blockStoreTx,+    blockStoreTxSTM,+    blockStoreTxHash,+    blockStoreTxHashSTM,+    blockStorePendingTxs,+    blockStorePendingTxsSTM,+    needsMoreBlocks,+  )+where++import Control.Monad+  ( forM,+    forM_,+    unless,+    void,+    when,+    (>=>),+  )+import Data.Bool (bool)+import Data.ByteString qualified as B+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.Maybe+  ( catMaybes,+    isJust,+  )+import Data.Serialize (encode)+import Data.String (fromString)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time.Clock+  ( NominalDiffTime,+    UTCTime,+    getCurrentTime,+  )+import Data.Time.Clock.POSIX+  ( getPOSIXTime,+    posixSecondsToUTCTime,+    utcTimeToPOSIXSeconds,+  )+import Data.Time.Format+  ( defaultTimeLocale,+    formatTime,+  )+import Haskoin+  ( Block (..),+    BlockHash (..),+    BlockHeader (..),+    BlockHeight,+    BlockNode (..),+    Ctx,+    GetData (..),+    InvType (..),+    InvVector (..),+    Message (..),+    Network (..),+    OutPoint (..),+    Tx (..),+    TxHash (..),+    TxIn (..),+    blockHashToHex,+    headerHash,+    txHash,+    txHashToHex,+  )+import Haskoin.Node+  ( Chain,+    LogLevel (..),+    Logger,+    OnlinePeer (..),+    Owner,+    Peer (..),+    PeerMgr,+    chainBlockMain,+    chainGetAncestor,+    chainGetBest,+    chainGetBlock,+    chainGetParents,+    getPeers,+    killPeer,+    logAt,+    newOwner,+    releasePeer,+    reservePeer,+    sendMessage,+  )+import Haskoin.Store.BlockStore.Pending (Pending, PendingTx (..))+import Haskoin.Store.BlockStore.Pending qualified as Pending+import Haskoin.Store.BlockStore.Sync+  ( Accept (..),+    Sync (..),+    needsMoreBlocks,+  )+import Haskoin.Store.BlockStore.Sync qualified as Sync+import Haskoin.Store.Common (StoreEvent (..), loopForever)+import Haskoin.Store.Data+import Haskoin.Store.Database.Mempool (wipeMempoolArea)+import Haskoin.Store.Database.Query (getMempoolBefore)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader+import Haskoin.Store.Database.Writer (MempoolChanges (..), Writer, runWriterMempool)+import Haskoin.Store.Database.Writer qualified as W+import Haskoin.Store.Logic (ImportException (..))+import Haskoin.Store.Logic.Layered+  ( deleteUnconfirmedTx,+    importBlock,+    initBest,+    newMempoolTx,+    revertBlock,+  )+import NQE+  ( InChan,+    Listen,+    Mailbox,+    Publisher,+    inboxToMailbox,+    newInbox,+    publish,+    query,+    receive,+    send,+    sendSTM,+  )+import System.Metrics.StatsD+import System.Random (randomRIO)+import UnliftIO+  ( Exception,+    STM,+    TVar,+    atomically,+    catch,+    link,+    modifyTVar,+    modifyTVar',+    newTVarIO,+    readTVar,+    readTVarIO,+    stateTVar,+    throwIO,+    try,+    withAsync,+  )+import UnliftIO.Concurrent (threadDelay)++-- | Messages handled by the block store process.+data BlockStoreMessage+  = -- | the node has a new best header+    BlockNewBest !BlockNode+  | -- | a peer connected+    BlockPeerConnect !Peer+  | -- | a peer disconnected+    BlockPeerDisconnect !Peer+  | -- | a peer sent a block+    BlockReceived !Peer !Block+  | -- | a peer could not find blocks+    BlockNotFound !Peer ![BlockHash]+  | -- | a peer sent a transaction+    TxRefReceived !Peer !Tx+  | -- | a peer announced transactions+    TxRefAvailable !Peer ![TxHash]+  | -- | clock tick for housekeeping; reply when done+    BlockPing !(Listen ())++-- | Fatal errors of the block store.+data BlockException+  = -- | the header of a block in the database is not known to the node+    BlockNotInChain !BlockHash+  | -- | the database has no best block+    Uninitialized+  | -- | the database is inconsistent+    CorruptDatabase+  | -- | the node has no header at this height below this block+    AncestorNotInChain !BlockHeight !BlockHash+  | -- | importing mempool transactions failed+    MempoolImportFailed+  deriving (Show, Eq, Ord, Exception)++-- | Block store process state.+data BlockStore = BlockStore+  { -- | mailbox of the process+    mailbox :: !(Mailbox BlockStoreMessage),+    -- | configuration+    config :: !BlockStoreConfig,+    -- | state of the block download ("Haskoin.Store.BlockStore.Sync")+    sync :: !(TVar (Sync Peer)),+    -- | transactions received and waiting to be imported+    -- ("Haskoin.Store.BlockStore.Pending")+    pending :: !(TVar Pending),+    -- | StatsD metrics, if enabled+    metrics :: !(Maybe StoreMetrics),+    -- | labels of the peers whose mempool was already requested+    syncedMempool :: !(TVar (HashSet Text)),+    -- | who reserves the syncing peer, so that no one else releases it+    owner :: !Owner,+    -- | transactions that left the mempool unconfirmed since the store+    -- started, as many as 'mostForgotten'. The database keeps nothing of+    -- them, and peers go on announcing them: without this the store would+    -- fetch each one again, only to refuse it again.+    forgotten :: !(TVar (HashSet TxHash))+  }++-- | How many deleted transactions the store remembers not to fetch again.+-- Past that many it forgets them all and starts over, which costs a few+-- fetches that come to nothing.+mostForgotten :: Int+mostForgotten = 100000++-- | StatsD metrics of the block store.+data StoreMetrics = StoreMetrics+  { -- | height of the best block (@store.blocks@)+    blocks :: !StatGauge,+    -- | height of the best header (@store.headers@)+    headers :: !StatGauge,+    -- | transactions waiting to be imported (@store.queued_txs@)+    queuedTxs :: !StatGauge,+    -- | online peers (@store.peers@)+    peers :: !StatGauge,+    -- | transactions in the mempool (@store.mempool@)+    mempool :: !StatGauge,+    -- | milliseconds to import a block (@store.block@)+    block :: !StatTiming+  }++-- | Register the block store metrics, if StatsD is configured, with their current values.+newStoreMetrics :: BlockStoreConfig -> IO (Maybe StoreMetrics)+newStoreMetrics cfg =+  forM cfg.stats $ \s -> do+    m <- Q.getMempool cfg.db+    b <-+      Q.getBestBlock cfg.db >>= \case+        Nothing -> return 0+        Just h -> maybe 0 (.height) <$> Q.getBlock cfg.db h+    h <- chainGetBest cfg.chain+    p <- getPeers cfg.peerMgr+    blocks <- g s "blocks" (fromIntegral b)+    headers <- g s "headers" (fromIntegral h.height)+    queuedTxs <- g s "queued_txs" 0+    peers <- g s "peers" (length p)+    mempool <- g s "mempool" (length m)+    block <- t s "block" 0+    return StoreMetrics {..}+  where+    g s x = newStatGauge s ("store." <> x)+    t s x = newStatTiming s ("store." <> x)++-- | Update the gauge of the best block height.+setStoreHeight :: BlockStore -> IO ()+setStoreHeight self =+  forM_ self.metrics $ \m ->+    getBestBlock self >>= mapM_ (getBlock self >=> mapM_ (setGauge m.blocks . fromIntegral . (.height)))++-- | Update the gauge of the best header height.+setHeadersHeight :: BlockStore -> IO ()+setHeadersHeight self =+  forM_ self.metrics $ \m -> do+    n <- chainGetBest self.config.chain+    setGauge m.headers (fromIntegral n.height)++-- | Update the gauge of transactions waiting to be imported.+setPendingTxs :: BlockStore -> IO ()+setPendingTxs self =+  forM_ self.metrics $ \m -> do+    p <- readTVarIO self.pending+    setGauge m.queuedTxs (Pending.queued p)++-- | Update the gauge of online peers.+setPeersConnected :: BlockStore -> IO ()+setPeersConnected self =+  forM_ self.metrics $ \m -> do+    p <- getPeers self.config.peerMgr+    setGauge m.peers (length p)++-- | Record how long a successful block import took.+withBlockTiming :: BlockStore -> IO (Either l r) -> IO (Either l r)+withBlockTiming self go = do+  case self.metrics of+    Just m -> do+      start <- round . (* 1000) <$> getPOSIXTime+      go >>= \case+        Right x -> do+          end <- round . (* 1000) <$> getPOSIXTime+          addTiming m.block (end - start)+          return (Right x)+        Left y -> return (Left y)+    Nothing -> go++-- | Configuration for a block store.+data BlockStoreConfig = BlockStoreConfig+  { -- | secp256k1 context+    ctx :: !Ctx,+    -- | peer manager from running node+    peerMgr :: !PeerMgr,+    -- | chain from a running node+    chain :: !Chain,+    -- | listener for store events+    pub :: !(Publisher StoreEvent),+    -- | RocksDB database handle+    db :: !DatabaseReader,+    -- | network constants+    net :: !Network,+    -- | do not index new mempool transactions+    noMempool :: !Bool,+    -- | wipe mempool at start+    wipeMempool :: !Bool,+    -- | what to do with the best block right after it changes, and when the+    -- store starts, before anything else is written to the database: a+    -- member of a fleet takes a view of the database for the block here+    -- ('Haskoin.Store.Fleet.Member.noteBest')+    onBest :: !(BlockHash -> IO ()),+    -- | sync mempool from peers+    syncMempool :: !Bool,+    -- | blocks to request from the syncing peer in one batch+    blockWindow :: !Int,+    -- | delete transactions older than this number of days from mempool+    mempoolTimeout :: !Int,+    -- | disconnect syncing peer if inactive for this long+    peerTimeout :: !NominalDiffTime,+    -- | StatsD client, if metrics are enabled+    stats :: !(Maybe Stats),+    -- | log messages here+    logger :: !Logger+  }++-- | Log a message with the logger of the block store.+logDebug, logInfo, logWarn, logError :: BlockStore -> Text -> IO ()+logDebug self = logAt LevelDebug self.config.logger+logInfo self = logAt LevelInfo self.config.logger+logWarn self = logAt LevelWarn self.config.logger+logError self = logAt LevelError self.config.logger++-- | Run an import against the database, returning the import error if it fails.+runImport :: BlockStore -> Network -> Ctx -> (Writer -> IO a) -> IO (Either ImportException a)+runImport self net ctx f = fmap fst <$> runImportMempool self net ctx f++-- | Like 'runImport', but also return how the import changed the mempool.+-- The mempool gauge is kept up to date from those changes instead of+-- counting the whole mempool on every clock tick.+runImportMempool :: BlockStore -> Network -> Ctx -> (Writer -> IO a) -> IO (Either ImportException (a, MempoolChanges))+runImportMempool self net ctx f = do+  r <- try $ runWriterMempool net ctx self.config.db self.config.logger f+  forM_ r $ \(_, c) ->+    forM_+      self.metrics+      ( \m -> do+          incrementGauge m.mempool (length c.added)+          decrementGauge m.mempool (length c.removed)+      )+  return r++-- | Run an import and publish the transactions that it added to the+-- mempool or removed from it, then the new best block if there is one.+-- Transactions of the block are not published.+runImportNotify :: BlockStore -> Maybe Block -> (Writer -> IO a) -> IO (Either ImportException a)+runImportNotify self block f = do+  net <- getNetwork self+  ctx <- getCtx self+  runImportMempool self net ctx f >>= \case+    Left e -> return (Left e)+    Right (x, c) -> do+      let l = self.config.pub+      let deleted = filter notInBlock c.removed+      atomically . modifyTVar' self.forgotten $ \seen ->+        let more = foldr HashSet.insert seen deleted+         in if HashSet.size more > mostForgotten then HashSet.fromList deleted else more+      -- one that came back is not forgotten any more+      atomically (modifyTVar' self.forgotten (\seen -> foldr HashSet.delete seen c.added))+      forM_ deleted $ \h ->+        publish (StoreMempoolDelete h) l+      forM_ (filter notInBlock c.added) $ \h ->+        publish (StoreMempoolNew h) l+      forM_ block $ \b -> do+        -- before the event, so that whoever hears of the block finds what+        -- was done for it+        self.config.onBest (headerHash b.header)+        publish (StoreBestBlock (headerHash b.header)) l+      return (Right x)+  where+    inBlock = maybe HashSet.empty (HashSet.fromList . map txHash . (.txs)) block+    notInBlock = not . (`HashSet.member` inBlock)++-- | Run a database query from the block store.+-- | Best block in the database.+getBestBlock :: BlockStore -> IO (Maybe BlockHash)+getBestBlock self = Q.getBestBlock self.config.db++-- | Data of a block in the database.+getBlock :: BlockStore -> BlockHash -> IO (Maybe BlockData)+getBlock self = Q.getBlock self.config.db++-- | Data of a transaction in the database: confirmed, in the mempool, or+-- deleted from it and marked so.+getTxData :: BlockStore -> TxHash -> IO (Maybe TxData)+getTxData self h = L.withView self.config.db (`L.getTxData` h)++-- | Unspent output in the database, of the chain or of the mempool.+getUnspent :: BlockStore -> OutPoint -> IO (Maybe Unspent)+getUnspent self p = L.withView self.config.db (`L.getUnspent` p)++-- | Network of the block store.+getNetwork :: BlockStore -> IO Network+getNetwork self = return self.config.net++-- | Secp256k1 context of the block store.+getCtx :: BlockStore -> IO Ctx+getCtx self = return self.config.ctx++-- | Run block store process.+withBlockStore ::+  BlockStoreConfig ->+  (BlockStore -> IO a) ->+  IO a+withBlockStore cfg action = do+  pb <- newTVarIO Idle+  ts <- newTVarIO Pending.empty+  sy <- newTVarIO HashSet.empty+  inbox <- newInbox+  metrics <- newStoreMetrics cfg+  owner <- (newOwner "block store")+  forgotten <- newTVarIO HashSet.empty+  let r =+        BlockStore+          { mailbox = inboxToMailbox inbox,+            config = cfg,+            sync = pb,+            pending = ts,+            metrics = metrics,+            syncedMempool = sy,+            owner = owner,+            forgotten = forgotten+          }+  withAsync ((go r inbox)) $ \a -> do+    link a+    action r+  where+    go self inbox = do+      ini+      wipe+      run+      where+        -- the mempool is an area of its own, so wiping it is deleting the+        -- keys of that area: nothing of the chain has to be put right+        wipe = when cfg.wipeMempool $ do+          keys <- wipeMempoolArea cfg.db+          forM_ self.metrics $ \m -> setGauge m.mempool 0+          logAt LevelInfo cfg.logger $+            "Wiped the mempool: " <> cs (show keys) <> " keys deleted"+        ini = do+          net <- getNetwork self+          ctx <- getCtx self+          runImport self net ctx initBest >>= \case+            Left e -> do+              logError self $+                "Could not initialize: " <> cs (show e)+              throwIO e+            Right () -> getBestBlock self >>= mapM_ cfg.onBest+        run =+          withAsync (pingMe (inboxToMailbox inbox)) $ \a ->+            link a >> runBlockStoreLoop self inbox++-- | Handle messages forever.+runBlockStoreLoop :: (InChan mbox) => BlockStore -> mbox BlockStoreMessage -> IO b+runBlockStoreLoop self inbox =+  loopForever $ do+    logDebug self "Waiting for new event..."+    msg <- receive inbox+    processBlockStoreMessage self msg++-- | Whether the best block in the database is the best header of the node. If+-- so, release the syncing peer.+isInSync :: BlockStore -> IO Bool+isInSync self =+  getBestBlock self >>= \case+    Nothing -> do+      logError self "Block database uninitialized"+      throwIO Uninitialized+    Just bb -> do+      cb <- chainGetBest self.config.chain+      if headerHash cb.header == bb+        then clearSyncingState self >> return True+        else return False++-- | Run an action only if the database is in sync.+guardSync :: BlockStore -> IO () -> IO ()+guardSync self f = isInSync self >>= \s -> when s f++-- | Run an action only if the mempool is indexed.+guardMempool :: BlockStore -> IO () -> IO ()+guardMempool self f = unless self.config.noMempool f++-- | Ask a peer for its mempool, once per peer, if in sync and mempool sync is enabled.+syncMempool :: BlockStore -> Peer -> IO ()+syncMempool self p =+  guardMempool self . guardSync self $ do+    let s = self.config.syncMempool+    let m = self.syncedMempool+    y <- not . HashSet.member p.label <$> readTVarIO m+    when (s && y) $ do+      logDebug self $+        "Requesting mempool from peer: " <> p.label+      atomically . modifyTVar m $ HashSet.insert p.label+      MMempool `sendMessage` p++-- | Import a block from the syncing peer, then ask for more blocks or, if now in+-- sync, sync the mempool. Disconnects the peer if it is not the syncing peer, if+-- the block is unknown to the node, or if the block cannot be imported.+processBlock :: BlockStore -> Peer -> Block -> IO ()+processBlock self peer block =+  Sync.accept peer blockhash <$> readTVarIO self.sync >>= \case+    NotSyncingPeer -> refuse "Non-syncing peer"+    NotRequested -> refuse "Syncing peer"+    Requested ->+      getBlockNode self blockhash >>= \case+        Nothing -> do+          logError self $+            "Peer "+              <> peer.label+              <> " sent unknown block "+              <> blockHashToHex blockhash+          killPeer peer+        Just node -> do+          logDebug self $+            "Processing block "+              <> blockText node Nothing+              <> " from peer "+              <> peer.label+          ret <- withBlockTiming self $ runImportNotify self (Just block) (\wr -> importBlock wr block node)+          case ret of+            Left e -> failure e+            Right _ -> success node+  where+    header = block.header+    blockhash = headerHash header+    hexhash = blockHashToHex blockhash+    refuse who = do+      logError self $+        who+          <> " "+          <> peer.label+          <> " sent a block that was not requested: "+          <> hexhash+      killPeer peer+    success node = do+      logInfo self $+        "Best block: " <> blockText node (Just block)+      now <- getCurrentTime+      transition self $ \st -> ((), Sync.imported now blockhash st)+      isInSync self >>= \case+        False -> syncMe self+        True -> do+          updateOrphans self+          syncMempool self peer+    failure e = do+      logError self $+        "Error importing block "+          <> hexhash+          <> " from peer: "+          <> peer.label+          <> ": "+          <> cs (show e)+      killPeer peer++-- | Change the state of the block download with a transition of+-- "Haskoin.Store.BlockStore.Sync", atomically, and give its result.+transition :: BlockStore -> (Sync Peer -> (a, Sync Peer)) -> IO a+transition self = atomically . stateTVar self.sync++-- | Header of a block from the node.+getBlockNode :: BlockStore -> BlockHash -> IO (Maybe BlockNode)+getBlockNode self blockhash = do+  let ch = self.config.chain+  chainGetBlock ch blockhash++-- | Disconnect a peer that could not find requested blocks.+processNoBlocks :: BlockStore -> Peer -> [BlockHash] -> IO ()+processNoBlocks self p hs = do+  forM_ (zip [(1 :: Int) ..] hs) $ \(i, h) ->+    logError self $+      "Block "+        <> cs (show i)+        <> "/"+        <> cs (show (length hs))+        <> " "+        <> blockHashToHex h+        <> " not found by peer: "+        <> p.label+  killPeer p++-- | Queue a transaction received from a peer for import.+processTx :: BlockStore -> Peer -> Tx -> IO ()+processTx self p tx = guardMempool self $ do+  t <- getCurrentTime+  logDebug self $+    "Received tx "+      <> txHashToHex (txHash tx)+      <> " by peer: "+      <> p.label+  changePending self (Pending.received t tx HashSet.empty)++-- | Forget old requests, and drop transactions that waited for their parents+-- for too long.+pruneOrphans :: BlockStore -> IO ()+pruneOrphans self = guardMempool self $ do+  now <- getCurrentTime+  changePending self (Pending.prune now)++-- | Change the mempool transactions on their way into the database.+changePending :: BlockStore -> (Pending -> Pending) -> IO ()+changePending self f = do+  atomically $ modifyTVar' self.pending f+  setPendingTxs self++-- | Whether a transaction is queued or was requested recently.+isPending :: BlockStore -> TxHash -> IO Bool+isPending self th = Pending.isPending th <$> readTVarIO self.pending++-- | Take up to this many queued transactions that have no missing parents, in+-- dependency order, out of the queue.+pendingTxs :: BlockStore -> Int -> IO [PendingTx]+pendingTxs self i = do+  selected <- atomically $ stateTVar self.pending (Pending.takeReady i)+  setPendingTxs self+  return selected++-- | Drop queued transactions that are already in the database, and recompute the+-- missing parents of the others.+updateOrphans :: BlockStore -> IO ()+updateOrphans self = do+  orphans <- Pending.waiting <$> readTVarIO self.pending+  updates <- forM orphans $ \p -> do+    let h = txHash p.tx+    exists h >>= \case+      True -> return (h, Nothing)+      False -> do+        found <- catMaybes <$> mapM (getUnspent self . (.outpoint)) p.tx.inputs+        let have = HashSet.fromList (map (.outpoint.hash) found)+        return (h, Just (HashSet.difference p.deps have))+  changePending self (Pending.refresh (HashMap.fromList updates))+  where+    exists th =+      getTxData self th >>= \case+        Nothing -> return False+        Just TxData {deleted = True} -> return False+        Just TxData {deleted = False} -> return True++-- | Whether the store has a transaction, or deleted it not long ago: in+-- both cases there is nothing to fetch.+known :: BlockStore -> TxHash -> IO (Maybe ())+known self h =+  HashSet.member h <$> readTVarIO self.forgotten >>= \case+    True -> return (Just ())+    False -> (() <$) <$> getTxData self h++-- | Queue a transaction that spends unknown outputs, recording which parents are missing.+newOrphanTx ::+  BlockStore ->+  UTCTime ->+  Tx ->+  Writer ->+  IO ()+newOrphanTx self time tx wr = do+  logAt LevelDebug self.config.logger $+    "Import tx "+      <> txHashToHex (txHash tx)+      <> ": Orphan"+  unspents <- catMaybes <$> mapM (W.getUnspent wr) prevs+  let have = HashSet.fromList (map (.outpoint) unspents)+      missing = HashSet.map (.hash) (HashSet.difference (HashSet.fromList prevs) have)+  atomically . modifyTVar' self.pending $ Pending.received time tx missing+  where+    prevs = map (.outpoint) tx.inputs++-- | Import a transaction into the mempool. Returns 'True' if imported. A+-- transaction that spends unknown outputs is queued as an orphan; other errors+-- are ignored.+importMempoolTx :: BlockStore -> UTCTime -> Tx -> Writer -> IO Bool+importMempoolTx block_read time tx wr =+  catch new_mempool_tx handle_error+  where+    tx_hash = txHash tx+    handle_error :: ImportException -> IO Bool+    handle_error Orphan = do+      newOrphanTx block_read time tx wr+      return False+    handle_error _ = return False+    seconds = floor (utcTimeToPOSIXSeconds time)+    new_mempool_tx = do+      t <- newMempoolTx wr tx seconds+      logAt LevelInfo block_read.config.logger $+        bool "Already have" "Imported" t+          <> " tx "+          <> txHashToHex (txHash tx)+      when t . atomically . modifyTVar' block_read.pending $ Pending.imported tx_hash+      return t++-- | Import up to 2000 queued transactions.+processMempool :: BlockStore -> IO ()+processMempool self = guardMempool self $ do+  txs <- pendingTxs self 2000+  let block_read = self+  unless (null txs) $ import_txs block_read txs+  where+    run_import block_read wr p =+      importMempoolTx block_read p.time p.tx wr+    import_txs block_read txs =+      runImportNotify self Nothing (\wr -> mapM (run_import block_read wr) txs) >>= \case+        Left e -> report_error e+        Right _ -> return ()+    report_error e = do+      logError self $+        "Error processing mempool: " <> cs (show e)+      throwIO e++-- | Delete up to 1000 mempool transactions older than the configured number of days.+pruneMempool :: BlockStore -> IO ()+pruneMempool self =+  guardMempool self $ do+    let days = self.config.mempoolTimeout+    when (days > 0) $ do+      time <- (floor . utcTimeToPOSIXSeconds) <$> getCurrentTime+      let thresh = time - (fromIntegral days * 24 * 60 * 60)+      -- runs on every clock tick: read only the expired entries, oldest+      -- first, from the index by time+      let db = self.config.db+      txs <- map snd <$> (getMempoolBefore db thresh 1000)+      unless (null txs) $ do+        logInfo self $+          "Deleting " <> cs (show (length txs)) <> " old transactions from mempool"+        runImportNotify self Nothing (\wr -> mapM_ (deleteUnconfirmedTx wr False) txs) >>= \case+          Left e ->+            logError self $+              "Error pruning mempool: " <> cs (show e)+          Right () -> return ()++-- | Request from a peer the announced transactions that are neither queued,+-- recently requested nor in the database. Only while in sync.+processTxs :: BlockStore -> Peer -> [TxHash] -> IO ()+processTxs self p hs = guardMempool self . guardSync self $ do+  logDebug self $+    "Received inventory with "+      <> cs (show (length hs))+      <> " transactions from peer: "+      <> p.label+  xs <- catMaybes <$> zip_counter process_tx+  unless (null xs) $ go xs+  where+    len = length hs+    zip_counter = forM (zip [(1 :: Int) ..] hs) . uncurry+    process_tx i h =+      isPending self h >>= \case+        True -> do+          logDebug self $+            "Tx "+              <> cs (show i)+              <> "/"+              <> cs (show len)+              <> " "+              <> txHashToHex h+              <> ": "+              <> "Already Pending"+          return Nothing+        False ->+          known self h >>= \case+            Just _ -> do+              logDebug self $+                "Tx "+                  <> cs (show i)+                  <> "/"+                  <> cs (show len)+                  <> " "+                  <> txHashToHex h+                  <> ": "+                  <> "Already Imported"+              return Nothing+            Nothing -> do+              logDebug self $+                "Tx "+                  <> cs (show i)+                  <> "/"+                  <> cs (show len)+                  <> " "+                  <> txHashToHex h+                  <> ": "+                  <> "Requesting"+              return (Just h)+    go xs = do+      now <- getCurrentTime+      changePending self (Pending.requested now xs)+      let net = self.config.net+      let inv = if net.segWit then InvWitnessTx else InvTx+          vec = map (InvVector inv . (.get)) xs+          msg = MGetData (GetData vec)+      msg `sendMessage` p++-- | Disconnect the syncing peer if it has been silent for too long.+checkTime :: BlockStore -> IO ()+checkTime self = do+  now <- getCurrentTime+  Sync.timedOut self.config.peerTimeout now <$> readTVarIO self.sync >>= \case+    Nothing -> return ()+    Just p -> do+      logError self $ "Syncing peer timeout: " <> p.label+      killPeer p++-- | Revert blocks until the best block in the database is in the main chain of headers.+revertToMainChain :: BlockStore -> IO ()+revertToMainChain self = do+  h <- headerHash . (.header) <$> getBest self+  let ch = self.config.chain+  net <- getNetwork self+  ctx <- getCtx self+  (chainBlockMain ch h) >>= \x -> unless x $ do+    logWarn self $+      "Reverting best block: "+        <> blockHashToHex h+    runImport self net ctx (`revertBlock` h) >>= \case+      Left e -> do+        logError self $+          "Could not revert block "+            <> blockHashToHex h+            <> ": "+            <> cs (show e)+        throwIO e+      Right () -> do+        getBestBlock self >>= mapM_ self.config.onBest+        transition self $ \st -> ((), Sync.forgetRequests st)+    revertToMainChain self++-- | Header of the best block in the database.+getBest :: BlockStore -> IO BlockNode+getBest self = do+  bb <-+    getBestBlock self >>= \case+      Just b -> return b+      Nothing -> do+        logError self "No best block set"+        throwIO Uninitialized+  let ch = self.config.chain+  (chainGetBlock ch bb) >>= \case+    Just x -> return x+    Nothing -> do+      logError self $+        "Header not found for best block: "+          <> blockHashToHex bb+      throwIO (BlockNotInChain bb)++-- | Header of the last block requested from the syncing peer, or of the best block if none.+getSyncBest :: BlockStore -> IO BlockNode+getSyncBest self = do+  bb <-+    Sync.lastRequested <$> readTVarIO self.sync >>= \case+      Just h -> return h+      Nothing ->+        getBestBlock self >>= \case+          Just b -> return b+          Nothing -> do+            logError self "No best block set"+            throwIO Uninitialized+  chainGetBlock self.config.chain bb >>= \case+    Just x -> return x+    Nothing -> do+      logError self $+        "Header not found for block: "+          <> blockHashToHex bb+      throwIO (BlockNotInChain bb)++-- | The syncing peer, when it is time to ask it for the next batch of blocks.+shouldSync :: BlockStore -> IO (Maybe Peer)+shouldSync self =+  isInSync self >>= \case+    True -> return Nothing+    False -> Sync.wantsMore (blockWindow self) <$> readTVarIO self.sync++-- | Blocks to request from the syncing peer in one batch. Never below one,+-- or the store would never ask for any block.+blockWindow :: BlockStore -> Int+blockWindow self = max 1 self.config.blockWindow++-- | Ask the syncing peer for the next batch of blocks, if the window has room.+syncMe :: BlockStore -> IO ()+syncMe self = do+  revertToMainChain self+  shouldSync self >>= \case+    Nothing -> return ()+    Just p -> do+      bb <- getSyncBest self+      bh <- chainGetBest self.config.chain+      when (bb /= bh) $ do+        bns <- sel bb bh+        logDebug self $+          "Requesting "+            <> fromString (show (length bns))+            <> " blocks from peer: "+            <> p.label+        transition self $ \st -> ((), Sync.request (map (headerHash . (.header)) bns) st)+        MGetData (GetData (map inv bns)) `sendMessage` p+  where+    inv = InvVector kind . (.get) . headerHash . (.header)+    kind = if self.config.net.segWit then InvWitnessBlock else InvBlock+    -- The blocks right after the last requested one, up to one window or+    -- the chain tip, whichever comes first.+    sel bb bh = do+      top <- findAncestor self (min (bb.height + fromIntegral (blockWindow self)) bh.height) bh+      ps <- chainGetParents self.config.chain (bb.height + 1) top+      return $ ps <> [top]++-- | Ancestor of a header at a height. Throws 'AncestorNotInChain' if missing.+findAncestor :: BlockStore -> BlockHeight -> BlockNode -> IO BlockNode+findAncestor self height target = do+  let ch = self.config.chain+  (chainGetAncestor ch height target) >>= \case+    Just ancestor -> return ancestor+    Nothing -> do+      let h = headerHash target.header+      logError self $+        "Could not find header for ancestor of block "+          <> blockHashToHex h+          <> " at height "+          <> cs (show target.height)+      throwIO $ AncestorNotInChain height h++-- | Forget a peer: release it if it was the syncing peer, and forget that its mempool was requested.+finishPeer :: BlockStore -> Peer -> IO ()+finishPeer self p = do+  released <- transition self (Sync.release p)+  when released $ do+    logDebug self $ "Releasing peer: " <> p.label+    void (releasePeer self.owner p)+  atomically $ modifyTVar self.syncedMempool $ HashSet.delete p.label++-- | Make a peer the syncing peer if there is none and the peer is not busy.+trySetPeer :: BlockStore -> Peer -> IO Bool+trySetPeer self p =+  isJust . Sync.syncingPeer <$> readTVarIO self.sync >>= \case+    True -> return False+    False ->+      reservePeer self.owner p >>= \case+        False -> return False+        True -> do+          now <- getCurrentTime+          started <- transition self $ \st -> case Sync.start now p st of+            Just st' -> (True, st')+            Nothing -> (False, st)+          if started+            then logDebug self ("Locked peer: " <> p.label)+            else void (releasePeer self.owner p)+          return started++-- | If not in sync and there is no syncing peer, pick the first free peer and start syncing.+trySyncing :: BlockStore -> IO ()+trySyncing self =+  isInSync self >>= \case+    True -> return ()+    False ->+      isJust . Sync.syncingPeer <$> readTVarIO self.sync >>= \case+        True -> return ()+        False -> getPeers self.config.peerMgr >>= recurse . map (.mailbox)+  where+    recurse [] = return ()+    recurse (p : ps) =+      trySetPeer self p >>= \case+        False -> recurse ps+        True -> syncMe self++-- | Start syncing against a newly connected peer if needed, or request its mempool if in sync.+trySyncingPeer :: BlockStore -> Peer -> IO ()+trySyncingPeer self p =+  isInSync self >>= \case+    True -> syncMempool self p+    False ->+      trySetPeer self p >>= \case+        False -> return ()+        True -> syncMe self++-- | Release the syncing peer, if any.+clearSyncingState :: BlockStore -> IO ()+clearSyncingState self =+  Sync.syncingPeer <$> readTVarIO self.sync >>= mapM_ (finishPeer self)++-- | Handle a message sent to the block store.+processBlockStoreMessage :: BlockStore -> BlockStoreMessage -> IO ()+processBlockStoreMessage self (BlockNewBest node) = do+  logDebug self $+    "New best block mined at height "+      <> cs (show node.height)+      <> ": "+      <> blockHashToHex (headerHash node.header)+  trySyncing self+processBlockStoreMessage self (BlockPeerConnect p) = do+  logDebug self $+    "New peer connected: " <> p.label+  trySyncingPeer self p+processBlockStoreMessage self (BlockPeerDisconnect p) = do+  logDebug self $+    "Peer disconnected: " <> p.label+  finishPeer self p+processBlockStoreMessage self (BlockReceived p b) = do+  logDebug self $+    "Received block: "+      <> blockHashToHex (headerHash b.header)+  processBlock self p b+processBlockStoreMessage self (BlockNotFound p bs) = do+  logDebug self $+    "Blocks not found by peer "+      <> p.label+      <> ": "+      <> T.unwords (map blockHashToHex bs)+  processNoBlocks self p bs+processBlockStoreMessage self (TxRefReceived p tx) = do+  logDebug self $+    "Transaction received from peer "+      <> p.label+      <> ": "+      <> txHashToHex (txHash tx)+  processTx self p tx+processBlockStoreMessage self (TxRefAvailable p ts) = do+  logDebug self $+    "Transactions available from peer "+      <> p.label+      <> ": "+      <> T.unwords (map txHashToHex ts)+  processTxs self p ts+processBlockStoreMessage self (BlockPing r) = do+  logDebug self "Internal clock event"+  setStoreHeight self+  setHeadersHeight self+  setPendingTxs self+  setPeersConnected self+  trySyncing self+  processMempool self+  pruneOrphans self+  pruneMempool self+  checkTime self+  atomically (r ())++-- | Send a clock tick to the block store every 0.1 to 1 second, waiting for each to be handled.+pingMe :: Mailbox BlockStoreMessage -> IO ()+pingMe mbox =+  loopForever $ do+    BlockPing `query` mbox+    delay <- randomRIO (100 * 1000, 1000 * 1000)+    threadDelay delay++-- | Tell the block store that a peer connected.+blockStorePeerConnect :: Peer -> BlockStore -> IO ()+blockStorePeerConnect peer store =+  BlockPeerConnect peer `send` store.mailbox++-- | Tell the block store that a peer disconnected.+blockStorePeerDisconnect ::+  Peer -> BlockStore -> IO ()+blockStorePeerDisconnect peer store =+  BlockPeerDisconnect peer `send` store.mailbox++-- | Tell the block store that the node has a new best header.+blockStoreHead ::+  BlockNode -> BlockStore -> IO ()+blockStoreHead node store =+  BlockNewBest node `send` store.mailbox++-- | Give the block store a block received from a peer.+blockStoreBlock ::+  Peer -> Block -> BlockStore -> IO ()+blockStoreBlock peer block store =+  BlockReceived peer block `send` store.mailbox++-- | Tell the block store that a peer could not find blocks.+blockStoreNotFound ::+  Peer -> [BlockHash] -> BlockStore -> IO ()+blockStoreNotFound peer blocks store =+  BlockNotFound peer blocks `send` store.mailbox++-- | Give the block store a transaction received from a peer.+blockStoreTx ::+  Peer -> Tx -> BlockStore -> IO ()+blockStoreTx peer tx store =+  TxRefReceived peer tx `send` store.mailbox++-- | Tell the block store that a peer announced transactions.+blockStoreTxHash ::+  Peer -> [TxHash] -> BlockStore -> IO ()+blockStoreTxHash peer txhashes store =+  TxRefAvailable peer txhashes `send` store.mailbox++-- | 'STM' version of 'blockStorePeerConnect'.+blockStorePeerConnectSTM ::+  Peer -> BlockStore -> STM ()+blockStorePeerConnectSTM peer store =+  BlockPeerConnect peer `sendSTM` store.mailbox++-- | 'STM' version of 'blockStorePeerDisconnect'.+blockStorePeerDisconnectSTM ::+  Peer -> BlockStore -> STM ()+blockStorePeerDisconnectSTM peer store =+  BlockPeerDisconnect peer `sendSTM` store.mailbox++-- | 'STM' version of 'blockStoreHead'.+blockStoreHeadSTM ::+  BlockNode -> BlockStore -> STM ()+blockStoreHeadSTM node store =+  BlockNewBest node `sendSTM` store.mailbox++-- | 'STM' version of 'blockStoreBlock'.+blockStoreBlockSTM ::+  Peer -> Block -> BlockStore -> STM ()+blockStoreBlockSTM peer block store =+  BlockReceived peer block `sendSTM` store.mailbox++-- | 'STM' version of 'blockStoreNotFound'.+blockStoreNotFoundSTM ::+  Peer -> [BlockHash] -> BlockStore -> STM ()+blockStoreNotFoundSTM peer blocks store =+  BlockNotFound peer blocks `sendSTM` store.mailbox++-- | 'STM' version of 'blockStoreTx'.+blockStoreTxSTM ::+  Peer -> Tx -> BlockStore -> STM ()+blockStoreTxSTM peer tx store =+  TxRefReceived peer tx `sendSTM` store.mailbox++-- | 'STM' version of 'blockStoreTxHash'.+blockStoreTxHashSTM ::+  Peer -> [TxHash] -> BlockStore -> STM ()+blockStoreTxHashSTM peer txhashes store =+  TxRefAvailable peer txhashes `sendSTM` store.mailbox++-- | Number of transactions queued or requested by the block store.+blockStorePendingTxs ::+  BlockStore -> IO Int+blockStorePendingTxs =+  atomically . blockStorePendingTxsSTM++-- | 'STM' version of 'blockStorePendingTxs'.+blockStorePendingTxsSTM ::+  BlockStore -> STM Int+blockStorePendingTxsSTM store = Pending.size <$> readTVar store.pending  -- | Describe a block for logs: height, time, hash and, if the block is given, size. blockText :: BlockNode -> Maybe Block -> Text
+ src/Haskoin/Store/BlockStore/Pending.hs view
@@ -0,0 +1,166 @@+-- |+-- Module      : Haskoin.Store.BlockStore.Pending+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Mempool transactions on their way into the database, as a pure value with+-- transitions.+--+-- A transaction that a peer announces goes through these states:+--+-- @+--   announced ──► requested ──► received ──► ready ──► taken for import+--                    │              │  (missing parents arrive)+--                    ▼              ▼+--                forgotten      dropped+--             (after 20 s)   (after an hour)+-- @+--+-- It is requested once, and not again while the request is fresh. Once+-- received, it waits until no parent is missing, and is then taken for+-- import together with other ready transactions, parents first.+--+-- = Why a pure value+--+-- The block store kept the queue and the recent requests in two variables,+-- changed by several functions, and forgot each request in a thread of its+-- own that slept for twenty seconds: one thread per transaction requested.+-- Here every change is a function, the clock is an argument, and requests+-- are forgotten by 'prune' on the clock tick of the block store, like the+-- transactions that waited too long.+module Haskoin.Store.BlockStore.Pending+  ( -- * State+    Pending,+    PendingTx (..),+    empty,+    isPending,+    queued,+    size,+    waiting,++    -- * Transitions+    requested,+    received,+    takeReady,+    imported,+    refresh,+    prune,++    -- * Timeouts+    requestTimeout,+    orphanTimeout,+  )+where++import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.Maybe (mapMaybe)+import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime)+import Haskoin (Tx, TxHash, txHash)+import Haskoin.Store.Common (sortTxs)++-- | Transaction received from a peer and waiting to be imported.+data PendingTx = PendingTx+  { -- | when it was received+    time :: !UTCTime,+    -- | the transaction+    tx :: !Tx,+    -- | parent transactions that are missing; empty if it can be imported+    deps :: !(HashSet TxHash)+  }+  deriving stock (Show, Eq, Ord)++-- | Transactions requested from peers and received from them.+data Pending = Pending+  { -- | received, by hash+    txs :: !(HashMap TxHash PendingTx),+    -- | requested and not received yet, with when+    asked :: !(HashMap TxHash UTCTime)+  }+  deriving stock (Show, Eq)++-- | How long a request keeps the transaction from being requested again.+requestTimeout :: NominalDiffTime+requestTimeout = 20++-- | How long a transaction waits for missing parents.+orphanTimeout :: NominalDiffTime+orphanTimeout = 3600++-- | Nothing requested or received.+empty :: Pending+empty = Pending HashMap.empty HashMap.empty++-- | Whether a transaction is received or requested, so that it is not+-- requested again.+isPending :: TxHash -> Pending -> Bool+isPending h p = HashMap.member h p.txs || HashMap.member h p.asked++-- | How many transactions are received and not taken for import yet.+queued :: Pending -> Int+queued p = HashMap.size p.txs++-- | How many transactions are received or requested.+size :: Pending -> Int+size p = HashMap.size (HashMap.union (() <$ p.txs) (() <$ p.asked))++-- | Received transactions that have missing parents.+waiting :: Pending -> [PendingTx]+waiting p = filter (not . null . (.deps)) (HashMap.elems p.txs)++-- | Transactions were requested.+requested :: UTCTime -> [TxHash] -> Pending -> Pending+requested now hs p = p {asked = HashMap.union (HashMap.fromList [(h, now) | h <- hs]) p.asked}++-- | A transaction was received, with the parents that it spends and the+-- database does not have. It is no longer a request.+received :: UTCTime -> Tx -> HashSet TxHash -> Pending -> Pending+received now tx deps p =+  Pending+    { txs = HashMap.insert h (PendingTx now tx deps) p.txs,+      asked = HashMap.delete h p.asked+    }+  where+    h = txHash tx++-- | Take up to this many transactions with no missing parents, parents+-- before children.+takeReady :: Int -> Pending -> ([PendingTx], Pending)+takeReady n p = (taken, p {txs = foldr (HashMap.delete . txHash . (.tx)) p.txs taken})+  where+    ready = HashMap.filter (null . (.deps)) p.txs+    taken =+      take n $+        mapMaybe ((`HashMap.lookup` ready) . txHash . snd) (sortTxs (map (.tx) (HashMap.elems ready)))++-- | A transaction is in the database: transactions waiting for it no longer+-- miss it.+imported :: TxHash -> Pending -> Pending+imported h p = p {txs = HashMap.map (\t -> t {deps = HashSet.delete h t.deps}) p.txs}++-- | After a block, give each waiting transaction what the database now+-- says about it: 'Nothing' if it is in the database already, so it is+-- dropped, or the parents that it still misses.+refresh :: HashMap TxHash (Maybe (HashSet TxHash)) -> Pending -> Pending+refresh updates p = p {txs = HashMap.mapMaybeWithKey update p.txs}+  where+    update h t = case HashMap.lookup h updates of+      Nothing -> Just t+      Just Nothing -> Nothing+      Just (Just deps) -> Just t {deps = deps}++-- | Forget requests older than 'requestTimeout', and drop transactions+-- received longer than 'orphanTimeout' ago.+prune :: UTCTime -> Pending -> Pending+prune now p =+  Pending+    { txs = HashMap.filter (\t -> age t.time <= orphanTimeout) p.txs,+      asked = HashMap.filter (\t -> age t <= requestTimeout) p.asked+    }+  where+    age t = now `diffUTCTime` t
+ src/Haskoin/Store/BlockStore/Sync.hs view
@@ -0,0 +1,178 @@+-- |+-- Module      : Haskoin.Store.BlockStore.Sync+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The state of the block download, as a pure state machine.+--+-- The block store downloads blocks from one peer at a time. It is either+-- 'Idle', with no syncing peer, or 'Syncing' from one peer, with the blocks+-- requested from it and not yet imported, in chain order. Every change of+-- that state is one of the transitions of this module, and the block store+-- only carries out what they decide: lock or release a peer, send a request,+-- disconnect a peer.+--+-- = Why a state machine+--+-- The block store used to keep this state in a @TVar (Maybe Syncing)@ that a+-- dozen functions changed in place, each with its own check of who the peer+-- was and what had been asked. Bugs in such code, like a peer left locked or+-- a block accepted that nobody asked for, show up as a sync that stops for no+-- visible reason. Pure transitions can be tested on their own, with any+-- sequence of events, and the rules are in one place:+--+-- * There is at most one syncing peer, and only 'start' sets one, from 'Idle'.+-- * Only blocks requested from the syncing peer are accepted ('accept'), so a+--   block from another peer, or one nobody asked for, is a reason to+--   disconnect the peer that sent it.+-- * A block is requested at most once while it is outstanding ('request').+-- * The peer times out when it has delivered nothing for a while+--   ('timedOut'); delivering a block or starting resets the clock.+--+-- The peer is a type parameter so that the tests can use plain numbers.+module Haskoin.Store.BlockStore.Sync+  ( -- * State+    Sync (..),+    Download (..),+    syncingPeer,+    outstanding,+    lastRequested,++    -- * Transitions+    start,+    release,+    Accept (..),+    accept,+    imported,+    request,+    forgetRequests,++    -- * Decisions+    wantsMore,+    needsMoreBlocks,+    timedOut,+  )+where++import Data.List (delete)+import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime)+import Haskoin (BlockHash)++-- | State of the block download from peers of type @p@.+data Sync p+  = -- | no syncing peer+    Idle+  | -- | downloading from a peer+    Syncing !(Download p)+  deriving stock (Eq, Show)++-- | A download from the syncing peer.+data Download p = Download+  { -- | the peer, locked for the block store+    peer :: !p,+    -- | when it last delivered a block, or when syncing started+    time :: !UTCTime,+    -- | blocks requested and not yet imported, in chain order+    blocks :: ![BlockHash]+  }+  deriving stock (Eq, Show)++-- | The syncing peer, if any.+syncingPeer :: Sync p -> Maybe p+syncingPeer Idle = Nothing+syncingPeer (Syncing d) = Just d.peer++-- | Blocks requested from the syncing peer and not yet imported.+outstanding :: Sync p -> [BlockHash]+outstanding Idle = []+outstanding (Syncing d) = d.blocks++-- | The last block requested, if any is outstanding: the next request+-- starts after it.+lastRequested :: Sync p -> Maybe BlockHash+lastRequested s = case outstanding s of+  [] -> Nothing+  bs -> Just (last bs)++-- | Start syncing from a peer, if there is no syncing peer yet. The caller+-- locks the peer first, and frees it again if this gives 'Nothing'.+start :: UTCTime -> p -> Sync p -> Maybe (Sync p)+start now p Idle = Just (Syncing (Download p now []))+start _ _ (Syncing _) = Nothing++-- | Stop syncing from a peer, when it disconnects or the store is in sync.+-- Gives the new state, and whether the peer was the syncing peer, in which+-- case the caller frees it. Another peer leaves the state as it is.+release :: (Eq p) => p -> Sync p -> (Bool, Sync p)+release p (Syncing d)+  | d.peer == p = (True, Idle)+release _ s = (False, s)++-- | What to do with a block that a peer sent.+data Accept+  = -- | it was requested from this peer: import it+    Requested+  | -- | the peer is not the syncing peer+    NotSyncingPeer+  | -- | the syncing peer sent a block that is not outstanding+    NotRequested+  deriving stock (Eq, Show)++-- | Whether to import a block that a peer sent. Anything but 'Requested'+-- means the peer misbehaves, or that the requests were forgotten after a+-- reorganisation and the block is from the old chain.+accept :: (Eq p) => p -> BlockHash -> Sync p -> Accept+accept p h = \case+  Syncing d+    | d.peer /= p -> NotSyncingPeer+    | h `elem` d.blocks -> Requested+    | otherwise -> NotRequested+  Idle -> NotSyncingPeer++-- | A requested block was imported: it is no longer outstanding, and the+-- peer is not idle.+imported :: UTCTime -> BlockHash -> Sync p -> Sync p+imported _ _ Idle = Idle+imported now h (Syncing d) = Syncing d {time = now, blocks = delete h d.blocks}++-- | Blocks were requested from the syncing peer. Blocks already outstanding+-- are not added twice. Nothing changes when idle, as there is nobody to ask.+request :: [BlockHash] -> Sync p -> Sync p+request _ Idle = Idle+request hs (Syncing d) = Syncing d {blocks = foldl' add d.blocks hs}+  where+    add bs h = if h `elem` bs then bs else bs <> [h]++-- | Forget the outstanding requests, after blocks were reverted: they+-- continued a chain that is no longer the best. Blocks of them that still+-- arrive are 'NotRequested'.+forgetRequests :: Sync p -> Sync p+forgetRequests Idle = Idle+forgetRequests (Syncing d) = Syncing d {blocks = []}++-- | The syncing peer, if it is time to ask it for more blocks, given the+-- size of the window.+wantsMore :: Int -> Sync p -> Maybe p+wantsMore _ Idle = Nothing+wantsMore window (Syncing d)+  | needsMoreBlocks window (length d.blocks) = Just d.peer+  | otherwise = Nothing++-- | Whether to ask for the next batch, given the window size and how many+-- requested blocks are still waiting to be imported. Asking once fewer than+-- a quarter of the window remain keeps the peer busy, while the queue of+-- downloaded blocks never grows much beyond one window. The threshold is at+-- least one, so that a window smaller than four still refills once empty.+needsMoreBlocks :: Int -> Int -> Bool+needsMoreBlocks window n = n < max 1 (window `div` 4)++-- | The syncing peer, if it has delivered nothing for longer than the+-- timeout.+timedOut :: NominalDiffTime -> UTCTime -> Sync p -> Maybe p+timedOut _ _ Idle = Nothing+timedOut timeout now (Syncing d)+  | now `diffUTCTime` d.time > timeout = Just d.peer+  | otherwise = Nothing
src/Haskoin/Store/Cache.hs view
@@ -1,1761 +1,1534 @@-{-# LANGUAGE ApplicativeDo #-}-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}---- |--- Module      : Haskoin.Store.Cache--- Copyright   : (c) Haskoin Developers--- License     : MIT--- Maintainer  : jprupp@protonmail.ch--- Stability   : experimental--- Portability : POSIX------ Redis cache for extended public keys (xpubs).------ Answering a query about an xpub from the database means deriving its--- addresses and reading the index of every one of them, which is slow for--- wallets with many used addresses. When such an xpub is queried, its balances,--- transactions and unspent outputs are copied to Redis, and later queries are--- answered from there. Only xpubs with at least 'minAddrs' used addresses are--- cached, and only while the store is in sync.------ A cache writer process ('cacheWriter') keeps cached xpubs up to date as new--- blocks and mempool transactions arrive. When Redis holds more than 'maxKeys'--- keys, the xpubs used least recently are evicted.------ = Redis keys------ * @b\<xpub\>@: hash of derivation path to balance of each address.--- * @t\<xpub\>@: sorted set of transactions, scored by 'blockRefScore'.--- * @u\<xpub\>@: sorted set of unspent outputs, scored by 'blockRefScore'.--- * @a\<address\>@: xpub and derivation path of a cached address.--- * @max@: sorted set of cached xpubs, scored by time of last use.--- * @mempool@: sorted set of mempool transactions.--- * @head@: last block that the cache has processed.--- * @xlock-\<xpub\>@, @lock@, @i\<xpub\>@: locks.------ Here @\<xpub\>@ and @\<address\>@ stand for their binary serialization.--- = Why a cache, and why Redis------ Most xpubs are small and fast to answer from RocksDB. A few, such as the--- wallets of exchanges and payment processors, have thousands of used addresses--- and are queried constantly. Answering them from the database means deriving and--- reading every address each time. Keeping their balances, transactions and--- unspent outputs ready in sorted sets turns those queries into a single range--- read.------ Redis fits because sorted sets are exactly the shape of these queries, and--- because several store instances can share one cache. Only large xpubs are--- cached ('minAddrs'), because small ones gain nothing, and xpubs not used for a--- while are evicted, so the cache holds the working set rather than everything--- ever queried.------ Locks keep two store instances, or two requests, from building the cache of--- the same xpub at the same time. A request that cannot get a lock in time is--- answered from the database rather than made to wait.-module Haskoin.Store.Cache-  ( CacheConfig (..),-    CacheMetrics,-    CacheT,-    CacheError (..),-    newCacheMetrics,-    withCache,-    connectRedis,-    blockRefScore,-    scoreBlockRef,-    CacheWriter,-    CacheWriterInbox,-    cacheNewBlock,-    cacheNewTx,-    cacheSyncMempool,-    cacheWriter,-    cacheDelXPubs,-    isInCache,-  )-where--import Control.DeepSeq (NFData)-import Control.Monad (forM, forM_, unless, void, when)-import Control.Monad.Reader (ReaderT (..), asks)-import Control.Monad.Trans (lift)-import Data.Bits (shift, (.&.), (.|.))-import Data.ByteString (ByteString)-import Data.Default (def)-import Data.Either (fromRight, isRight, rights)-import Data.Functor ((<&>))-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as HashMap-import Data.HashSet qualified as HashSet-import Data.IntMap.Strict qualified as I-import Data.List (sort)-import Data.List.NonEmpty qualified as NonEmpty-import Data.Map.Strict qualified as Map-import Data.Maybe-  ( catMaybes,-    mapMaybe,-  )-import Data.Serialize (Serialize, decode, encode)-import Data.String.Conversions (cs)-import Data.Text (Text)-import Data.Time.Clock.POSIX (getPOSIXTime)-import Data.Word (Word32, Word64)-import Database.Redis-  ( Connection,-    Redis,-    RedisCtx,-    Reply,-    checkedConnect,-    defaultConnectInfo,-    hgetall,-    parseConnectInfo,-    zadd,-    zrangeWithscores,-    zrangebyscoreWithscoresLimit,-    zrem,-  )-import Database.Redis qualified as Redis-import GHC.Generics (Generic)-import Haskoin-  ( Address,-    BlockHash,-    BlockNode (..),-    Ctx,-    DerivPathI (..),-    KeyIndex,-    Network,-    OutPoint (..),-    Tx (..),-    TxHash,-    TxIn (..),-    TxOut (..),-    XPubKey,-    blockHashToHex,-    derivePubPath,-    eitherToMaybe,-    headerHash,-    pathToList,-    scriptToAddressBS,-    txHash,-    txHashToHex,-    xPubAddr,-    xPubCompatWitnessAddr,-    xPubExport,-    xPubWitnessAddr,-  )-import Haskoin.Node-  ( Chain,-    LogLevel (..),-    Logger,-    chainGetBest,-    chainGetBlock,-    chainGetParents,-    chainGetSplitBlock,-    logAt,-  )-import Haskoin.Store.Common-import Haskoin.Store.Data-import Haskoin.Store.Database.Reader (DatabaseReader, withDB)-import NQE-  ( Inbox,-    Listen,-    Mailbox,-    mailboxEmptySTM,-    query,-    receive,-    receiveSTM,-    requeueSTM,-    send,-  )-import System.Metrics.StatsD-import UnliftIO-  ( Exception,-    MonadIO,-    MonadUnliftIO,-    atomically,-    bracket,-    liftIO,-    throwIO,-    withAsync,-  )-import UnliftIO.Concurrent (threadDelay)---- | Run a Redis command. Throws 'RedisError' if Redis returns an error.-runRedis :: (MonadIO m) => Redis (Either Reply a) -> CacheX m a-runRedis action = do-  conn <- asks (.redis)-  logger <- asks (.logger)-  withRedis logger conn action---- | Run a Redis command on a connection. Logs and throws 'RedisError' if Redis returns an error.-withRedis ::-  (MonadIO m) => Logger -> Connection -> Redis (Either Reply a) -> m a-withRedis logger conn action =-  liftIO (Redis.runRedis conn action) >>= \case-    Right x -> return x-    Left e -> do-      logAt LevelError logger $ "Got error from Redis: " <> cs (show e)-      throwIO (RedisError e)---- | Cache configuration.-data CacheConfig = CacheConfig-  { -- | connection to Redis-    redis :: !Connection,-    -- | cache xpubs with at least this many used addresses-    minAddrs :: !Int,-    -- | evict xpubs when Redis holds more keys than this-    maxKeys :: !Integer,-    -- | header chain, used to know whether the store is in sync-    chain :: !Chain,-    -- | StatsD metrics, if enabled-    metrics :: !(Maybe CacheMetrics),-    -- | log messages here-    logger :: !Logger-  }---- | StatsD metrics of the cache.-data CacheMetrics = CacheMetrics-  { -- | xpub balance queries answered from the cache (@cache.hits@)-    hits :: !StatCounter,-    -- | xpub balance queries not in the cache (@cache.misses@)-    misses :: !StatCounter,-    -- | locks acquired (@cache.lock_acquired@)-    lockAcquired :: !StatCounter,-    -- | locks released (@cache.lock_released@)-    lockReleased :: !StatCounter,-    -- | locks that could not be acquired (@cache.lock_failed@)-    lockFailed :: !StatCounter,-    -- | xpub balances served (@cache.xpub_balances@)-    xBalances :: !StatCounter,-    -- | xpub unspent outputs served (@cache.xpub_unspents@)-    xUnspents :: !StatCounter,-    -- | xpub transactions served (@cache.xpub_txs@)-    xTxs :: !StatCounter,-    -- | xpub transaction counts served (@cache.xpub_tx_count@)-    xTxCount :: !StatCounter,-    -- | milliseconds to cache an xpub (@cache.index@)-    indexTime :: !StatTiming,-    -- | height of the last block processed by the cache (@cache.height@)-    height :: !StatGauge-  }---- | Register the cache metrics with a StatsD client.-newCacheMetrics ::-  (MonadIO m) =>-  Logger ->-  Stats ->-  Connection ->-  DatabaseReader ->-  m CacheMetrics-newCacheMetrics logger s r db = do-  h <- redisBest-  hits <- c "cache.hits"-  misses <- c "cache.misses"-  lockAcquired <- c "cache.lock_acquired"-  lockReleased <- c "cache.lock_released"-  lockFailed <- c "cache.lock_failed"-  indexTime <- d "cache.index"-  xBalances <- c "cache.xpub_balances"-  xUnspents <- c "cache.xpub_unspents"-  xTxs <- c "cache.xpub_txs"-  xTxCount <- c "cache.xpub_tx_count"-  height <- newStatGauge s "cache.height" h-  return CacheMetrics {..}-  where-    redisBest =-      maybe dbBest dbBlock =<< withRedis logger r redisGetHead-    dbBlock h =-      maybe 0 (fromIntegral . (.height)) <$> withDB db (getBlock h)-    dbBest =-      maybe (return 0) dbBlock =<< withDB db getBestBlock-    c x = newStatCounter s x 10-    d x = newStatTiming s x 10---- | Record how long an action takes in a timing metric.-withTiming ::-  (MonadUnliftIO m) =>-  (CacheMetrics -> StatTiming) ->-  CacheX m a ->-  CacheX m a-withTiming df go =-  asks (.metrics) >>= \case-    Nothing -> go-    Just m ->-      bracket-        time-        (end m)-        (const go)-  where-    time = round . (* 1000) <$> liftIO getPOSIXTime-    end m t1 = time >>= addTiming (df m) . subtract t1---- | Run an action with the metrics, if they are enabled.-withMetrics ::-  (MonadIO m) => (CacheMetrics -> CacheX m a) -> CacheX m ()-withMetrics go = asks (.metrics) >>= mapM_ go---- | Monad transformer that may use the cache. With 'Nothing', every query goes--- to the underlying store. Run it with 'withCache'.-type CacheT = ReaderT (Maybe CacheConfig)---- | Monad transformer that uses the cache.-type CacheX = ReaderT CacheConfig---- | Log a message with the logger of the cache.-logDebug, logInfo, logWarn, logError :: (MonadIO m) => Text -> CacheX m ()-logDebug = cacheLog LevelDebug-logInfo = cacheLog LevelInfo-logWarn = cacheLog LevelWarn-logError = cacheLog LevelError---- | Log a message at a level with the logger of the cache.-cacheLog :: (MonadIO m) => LogLevel -> Text -> CacheX m ()-cacheLog l msg = asks (.logger) >>= \lg -> logAt l lg msg---- | Errors of the cache.-data CacheError-  = -- | Redis returned an error-    RedisError Reply-  | -- | a Redis transaction failed-    RedisTxError !String-  | -- | the cache is inconsistent with the database-    LogicError !String-  deriving (Show, Eq, Generic, NFData, Exception)---- | Connect to Redis at a URL such as @redis:\/\/localhost:6379@, or with the--- default settings if the URL is empty. Calls 'error' if the URL is invalid.-connectRedis :: (MonadIO m) => String -> m Connection-connectRedis redisurl = do-  conninfo <--    if null redisurl-      then return defaultConnectInfo-      else case parseConnectInfo redisurl of-        Left e -> error e-        Right r -> return r-  liftIO (checkedConnect conninfo)--instance (StoreReadBase m) => StoreReadBase (CacheX m) where-  getCtx = lift getCtx-  getNetwork = lift getNetwork-  getBestBlock = lift getBestBlock-  getBlocksAtHeight = lift . getBlocksAtHeight-  getBlock = lift . getBlock-  getTxData = lift . getTxData-  getManyTxData = lift . getManyTxData-  getSpender = lift . getSpender-  getBalance = lift . getBalance-  getUnspent = lift . getUnspent-  getMempool = lift getMempool-  getMempoolPage = lift . getMempoolPage--instance (StoreReadBase m) => StoreReadBase (CacheT m) where-  getCtx = lift getCtx-  getNetwork = lift getNetwork-  getBestBlock = lift getBestBlock-  getBlocksAtHeight = lift . getBlocksAtHeight-  getBlock = lift . getBlock-  getTxData = lift . getTxData-  getManyTxData = lift . getManyTxData-  getSpender = lift . getSpender-  getBalance = lift . getBalance-  getUnspent = lift . getUnspent-  getMempool = lift getMempool-  getMempoolPage = lift . getMempoolPage--instance-  (MonadUnliftIO m, StoreReadExtra m) =>-  StoreReadExtra (CacheX m)-  where-  getBalances = lift . getBalances-  getAddressesTxs addrs = lift . getAddressesTxs addrs-  getAddressTxs addr = lift . getAddressTxs addr-  getAddressUnspents addr = lift . getAddressUnspents addr-  getAddressesUnspents addrs = lift . getAddressesUnspents addrs-  getMaxGap = lift getMaxGap-  getInitialGap = lift getInitialGap-  getNumTxData = lift . getNumTxData-  xPubTxs = getXPubTxs-  xPubTxCount = getXPubTxCount-  xPubBals = getXPubBalances-  xPubUnspents = getXPubUnspents--instance-  (MonadUnliftIO m, StoreReadExtra m) =>-  StoreReadExtra (CacheT m)-  where-  getBalances = lift . getBalances-  getAddressesTxs addrs = lift . getAddressesTxs addrs-  getAddressTxs addr = lift . getAddressTxs addr-  getAddressUnspents addr = lift . getAddressUnspents addr-  getAddressesUnspents addrs = lift . getAddressesUnspents addrs-  getMaxGap = lift getMaxGap-  getInitialGap = lift getInitialGap-  getNumTxData = lift . getNumTxData-  xPubBals xpub =-    ReaderT $ \case-      Nothing -> xPubBals xpub-      Just cfg -> runReaderT (xPubBals xpub) cfg-  xPubUnspents xpub xbals limits =-    ReaderT $ \case-      Nothing -> xPubUnspents xpub xbals limits-      Just cfg -> runReaderT (xPubUnspents xpub xbals limits) cfg-  xPubTxs xpub xbals limits =-    ReaderT $ \case-      Nothing -> xPubTxs xpub xbals limits-      Just cfg -> runReaderT (xPubTxs xpub xbals limits) cfg-  xPubTxCount xpub xbals =-    ReaderT $ \case-      Nothing -> xPubTxCount xpub xbals-      Just cfg -> runReaderT (xPubTxCount xpub xbals) cfg---- | Run queries with the cache, or without it if 'Nothing'.-withCache :: Maybe CacheConfig -> CacheT m a -> m a-withCache s f = runReaderT f s---- | Prefix of the keys of xpub balances.-balancesPfx :: ByteString-balancesPfx = "b"---- | Prefix of the keys of xpub transactions.-txSetPfx :: ByteString-txSetPfx = "t"---- | Prefix of the keys of xpub unspent outputs.-utxoPfx :: ByteString-utxoPfx = "u"---- | Prefix of the keys that mark an xpub as being cached.-idxPfx :: ByteString-idxPfx = "i"---- | Key of the lock held while caching an xpub.-xpubLock :: XPubSpec -> ByteString-xpubLock xpub = "xlock-" <> encode xpub---- | Run a read of the cache entries of an xpub in the same round trip to--- Redis as the check that the xpub is cached. Redis pipelines the two--- commands, since the reply of the first is not read before the second is--- sent. The read is 'Nothing' if the xpub is not cached; it then found no--- entries, as the keys of an xpub are all deleted together.-whenXPubCached ::-  (MonadIO m) =>-  XPubSpec ->-  Redis (Either Reply a) ->-  CacheX m (Maybe a)-whenXPubCached xpub action = do-  (cached, x) <- runRedis $ do-    e <- redisIsXPubCached xpub-    r <- action-    return $ (,) <$> e <*> r-  return $ if cached then Just x else Nothing---- | The score to start reading a sorted set of an xpub from, when it does--- not need a database lookup.-startScore :: Maybe Start -> Maybe (Maybe Double)-startScore Nothing = Just Nothing-startScore (Just (AtBlock h)) = Just (Just (blockRefScore (BlockRef h maxBound)))-startScore (Just AtTx {}) = Nothing---- | Transactions of an xpub, from the cache if it is cached, or from the store otherwise.-getXPubTxs ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  XPubSpec ->-  [XPubBal] ->-  Limits ->-  CacheX m [TxRef]-getXPubTxs xpub xbals limits =-  case startScore limits.start of-    Just score ->-      whenXPubCached xpub (get_txs score) >>= \case-        Just xs -> do-          touchKeys [xpub]-          hit (map (\(t, s) -> TxRef {txid = t, block = scoreBlockRef s}) xs)-        Nothing -> lift $ xPubTxs xpub xbals limits-    Nothing -> do-      cached <- isXPubCached xpub-      if cached-        then hit =<< cacheGetXPubTxs xpub limits-        else lift $ xPubTxs xpub xbals limits-  where-    get_txs score =-      getFromSortedSet-        (txSetPfx <> encode xpub)-        score-        limits.offset-        limits.limit-    hit txs = do-      withMetrics $ \s ->-        incrementCounter s.xTxs (length txs)-      return txs---- | Number of transactions of an xpub, from the cache if it is cached, or from the store otherwise.-getXPubTxCount ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  XPubSpec ->-  [XPubBal] ->-  CacheX m Word32-getXPubTxCount xpub xbals =-  whenXPubCached xpub (redisGetXPubTxCount xpub) >>= \case-    Just count -> do-      withMetrics $ \s ->-        incrementCounter s.xTxCount 1-      touchKeys [xpub]-      return (fromInteger count)-    Nothing -> lift $ xPubTxCount xpub xbals---- | Unspent outputs of an xpub, from the cache if it is cached, or from the store otherwise.-getXPubUnspents ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  XPubSpec ->-  [XPubBal] ->-  Limits ->-  CacheX m [XPubUnspent]-getXPubUnspents xpub xbals limits =-  case startScore limits.start of-    Just score ->-      whenXPubCached xpub (get_ops score) >>= \case-        Just xs -> do-          touchKeys [xpub]-          hit (map fst xs)-        Nothing -> lift $ xPubUnspents xpub xbals limits-    Nothing -> do-      cached <- isXPubCached xpub-      if cached-        then hit . map snd =<< cacheGetXPubUnspents xpub limits-        else lift $ xPubUnspents xpub xbals limits-  where-    get_ops score =-      getFromSortedSet-        (utxoPfx <> encode xpub)-        score-        limits.offset-        limits.limit-    hit ops = do-      uns <- catMaybes <$> lift (mapM getUnspent ops)-      net <- lift getNetwork-      ctx <- lift getCtx-      let f u =-            either-              (const Nothing)-              (\a -> Just (a, u))-              (scriptToAddressBS net ctx u.script)-          xm =-            let j x = (x.balance.address, x)-                k = (> 0) . (.balance.utxo)-             in HashMap.fromList $ map j $ filter k xbals-          g a = HashMap.lookup a xm-          h u x =-            XPubUnspent-              { unspent = u,-                path = x.path-              }-          us = mapMaybe f uns-          i a u = h u <$> g a-      withMetrics $ \s -> incrementCounter s.xUnspents (length us)-      return $ mapMaybe (uncurry i) us---- | Balances of an xpub, from the cache if it is cached. Otherwise read them from--- the store and cache the xpub if it has enough used addresses.-getXPubBalances ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  XPubSpec ->-  CacheX m [XPubBal]-getXPubBalances xpub =-  whenXPubCached xpub (redisGetXPubBalances xpub) >>= \case-    Just xbals -> do-      touchKeys [xpub]-      withMetrics $ \s -> do-        incrementCounter s.hits 1-        incrementCounter s.xBalances (length xbals)-      return xbals-    Nothing -> do-      withMetrics $ \s -> incrementCounter s.misses 1-      withRetryLock (xpubLock xpub) (load_and_cache xpub) >>= \case-        Just bals -> return bals-        Nothing -> do-          -- Another thread held the lock for too long: serve from the-          -- database without caching rather than wait any longer.-          logWarn "Could not acquire xpub lock, serving from database"-          lift $ xPubBals xpub-  where-    load_and_cache x = do-      bals <- lift $ xPubBals x-      newXPubC x bals-      return bals---- | Whether an xpub is cached. Always 'False' without a cache.-isInCache :: (MonadIO m) => XPubSpec -> CacheT m Bool-isInCache xpub =-  ReaderT $ \case-    Nothing -> return False-    Just cfg -> runReaderT (isXPubCached xpub) cfg---- | Whether an xpub is cached.-isXPubCached :: (MonadIO m) => XPubSpec -> CacheX m Bool-isXPubCached xpub = runRedis (redisIsXPubCached xpub)---- | Redis command to check whether an xpub is cached.-redisIsXPubCached :: (RedisCtx m f) => XPubSpec -> m (f Bool)-redisIsXPubCached xpub = Redis.exists (balancesPfx <> encode xpub)---- | Redis command to count the transactions of an xpub.-redisGetXPubTxCount :: (RedisCtx m f) => XPubSpec -> m (f Integer)-redisGetXPubTxCount xpub = Redis.zcard (txSetPfx <> encode xpub)---- | Page of the cached transactions of an xpub.-cacheGetXPubTxs ::-  (StoreReadBase m, MonadIO m) =>-  XPubSpec ->-  Limits ->-  CacheX m [TxRef]-cacheGetXPubTxs xpub limits =-  case limits.start of-    Nothing ->-      go1 Nothing-    Just (AtTx th) ->-      lift (getTxData th) >>= \case-        Just TxData {block = b@BlockRef {}} ->-          go1 $ Just (blockRefScore b)-        _ ->-          go2 th-    Just (AtBlock h) ->-      go1 (Just (blockRefScore (BlockRef h maxBound)))-  where-    go1 score = do-      xs <--        runRedis $-          getFromSortedSet-            (txSetPfx <> encode xpub)-            score-            limits.offset-            limits.limit-      touchKeys [xpub]-      return $ map (uncurry f) xs-    go2 hash = do-      xs <--        runRedis $-          getFromSortedSet-            (txSetPfx <> encode xpub)-            Nothing-            0-            0-      touchKeys [xpub]-      let xs' =-            if any ((== hash) . fst) xs-              then dropWhile ((/= hash) . fst) xs-              else []-      return $-        map (uncurry f) $-          l $-            drop (fromIntegral limits.offset) xs'-    l =-      if limits.limit > 0-        then take (fromIntegral limits.limit)-        else id-    f t s = TxRef {txid = t, block = scoreBlockRef s}---- | Page of the cached unspent outputs of an xpub, with their positions.-cacheGetXPubUnspents ::-  (StoreReadBase m, MonadIO m) =>-  XPubSpec ->-  Limits ->-  CacheX m [(BlockRef, OutPoint)]-cacheGetXPubUnspents xpub limits =-  case limits.start of-    Nothing ->-      go1 Nothing-    Just (AtTx th) ->-      lift (getTxData th) >>= \case-        Just TxData {block = b@BlockRef {}} ->-          go1 (Just (blockRefScore b))-        _ ->-          go2 th-    Just (AtBlock h) ->-      go1 (Just (blockRefScore (BlockRef h maxBound)))-  where-    go1 score = do-      xs <--        runRedis $-          getFromSortedSet-            (utxoPfx <> encode xpub)-            score-            limits.offset-            limits.limit-      touchKeys [xpub]-      return $ map (uncurry f) xs-    go2 hash = do-      xs <--        runRedis $-          getFromSortedSet-            (utxoPfx <> encode xpub)-            Nothing-            0-            0-      touchKeys [xpub]-      let xs' =-            if any ((== hash) . (.hash) . fst) xs-              then dropWhile ((/= hash) . (.hash) . fst) xs-              else []-      return $-        map (uncurry f) $-          l $-            drop (fromIntegral limits.offset) xs'-    l =-      if limits.limit > 0-        then take (fromIntegral limits.limit)-        else id-    f o s = (scoreBlockRef s, o)---- | Redis command to read the cached balances of an xpub, sorted by path.-redisGetXPubBalances :: (Functor f, RedisCtx m f) => XPubSpec -> m (f [XPubBal])-redisGetXPubBalances xpub =-  fmap (sort . map (uncurry f)) <$> getAllFromMap (balancesPfx <> encode xpub)-  where-    f p b = XPubBal {path = p, balance = b}---- | Score of a position in a Redis sorted set, such that sorting by score--- ascending sorts from newest to oldest. Mempool transactions get negative--- scores from their time, so they come first; confirmed ones get positive--- scores that decrease with height and position.------ One number has to order mempool and confirmed transactions in a single sorted--- set, newest first, exactly as the database does. Floating point is what Redis--- offers, so the height and position are packed into the 53 bits a double holds--- exactly.-blockRefScore :: BlockRef -> Double-blockRefScore BlockRef {height = h, position = p} =-  fromIntegral (0x001fffffffffffff - (h' .|. p'))-  where-    h' = (fromIntegral h .&. 0x07ffffff) `shift` 26 :: Word64-    p' = (fromIntegral p .&. 0x03ffffff) :: Word64-blockRefScore MemRef {timestamp = t} = negate t'-  where-    t' = fromIntegral (t .&. 0x001fffffffffffff)---- | Inverse of 'blockRefScore'.-scoreBlockRef :: Double -> BlockRef-scoreBlockRef s-  | s < 0 = MemRef {timestamp = n}-  | otherwise = BlockRef {height = h, position = p}-  where-    n = truncate (abs s) :: Word64-    m = 0x001fffffffffffff - n-    h = fromIntegral (m `shift` (-26))-    p = fromIntegral (m .&. 0x03ffffff)---- | Redis command to read members of a sorted set, decoded, with their scores,--- from a minimum score if given, skipping an offset, up to a count (@0@ for--- all).-getFromSortedSet ::-  (Applicative f, RedisCtx m f, Serialize a) =>-  ByteString ->-  Maybe Double ->-  Word32 ->-  Word32 ->-  m (f [(a, Double)])-getFromSortedSet key Nothing off 0 = do-  xs <- zrangeWithscores key (fromIntegral off) (-1)-  return $ do-    ys <- map (\(x, s) -> (,s) <$> decode x) <$> xs-    return (rights ys)-getFromSortedSet key Nothing off count = do-  xs <--    zrangeWithscores-      key-      (fromIntegral off)-      (fromIntegral off + fromIntegral count - 1)-  return $ do-    ys <- map (\(x, s) -> (,s) <$> decode x) <$> xs-    return (rights ys)-getFromSortedSet key (Just score) off 0 = do-  xs <--    zrangebyscoreWithscoresLimit-      key-      score-      (1 / 0)-      (fromIntegral off)-      (-1)-  return $ do-    ys <- map (\(x, s) -> (,s) <$> decode x) <$> xs-    return (rights ys)-getFromSortedSet key (Just score) off count = do-  xs <--    zrangebyscoreWithscoresLimit-      key-      score-      (1 / 0)-      (fromIntegral off)-      (fromIntegral count)-  return $ do-    ys <- map (\(x, s) -> (,s) <$> decode x) <$> xs-    return (rights ys)---- | Redis command to read all fields of a hash, decoded.-getAllFromMap ::-  (Functor f, RedisCtx m f, Serialize k, Serialize v) =>-  ByteString ->-  m (f [(k, v)])-getAllFromMap n = do-  fxs <- hgetall n-  return $ do-    xs <- fxs-    return-      [ (k, v)-      | (k', v') <- xs,-        let k = fromRight undefined $ decode k',-        let v = fromRight undefined $ decode v'-      ]---- | Messages handled by the cache writer.-data CacheWriterMessage-  = -- | a new block was imported-    CacheNewBlock-  | -- | a mempool transaction was added or removed-    CacheNewTx !TxHash-  | -- | internal: a batch of coalesced 'CacheNewTx' events-    CacheNewTxs ![TxHash]-  | -- | sync the mempool with the database, then reply-    CacheSyncMempool !(Listen ())---- | Inbox of the cache writer process.-type CacheWriterInbox = Inbox CacheWriterMessage---- | Mailbox to send messages to the cache writer.-type CacheWriter = Mailbox CacheWriterMessage---- | Xpub that a cached address belongs to.-data AddressXPub = AddressXPub-  { -- | the xpub-    spec :: !XPubSpec,-    -- | derivation path of the address below the xpub-    path :: ![KeyIndex]-  }-  deriving (Show, Eq, Generic, NFData, Serialize)---- | Key of the sorted set of mempool transactions.-mempoolSetKey :: ByteString-mempoolSetKey = "mempool"---- | Prefix of the keys of cached addresses.-addrPfx :: ByteString-addrPfx = "a"---- | Key of the last block processed by the cache.-bestBlockKey :: ByteString-bestBlockKey = "head"---- | Key of the sorted set of cached xpubs by time of last use.-maxKey :: ByteString-maxKey = "max"---- | Address of an extended public key for a type of address.-xPubAddrFunction :: Ctx -> DeriveType -> XPubKey -> Address-xPubAddrFunction ctx DeriveNormal = xPubAddr ctx-xPubAddrFunction ctx DeriveP2SH = xPubCompatWitnessAddr ctx-xPubAddrFunction ctx DeriveP2WPKH = xPubWitnessAddr ctx---- | Run the cache writer process: bring the cache up to date with the database,--- then handle messages forever. Notifications of new mempool transactions that--- pile up are handled together, up to 1000 at a time.-cacheWriter ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  CacheConfig ->-  CacheWriterInbox ->-  m ()-cacheWriter cfg inbox =-  runReaderT go cfg-  where-    go = do-      newBlockC-      syncMempoolC-      loopForever $ do-        logDebug "Awaiting event..."-        x <- receive inbox-        case x of-          CacheNewTx txid -> do-            -- Coalesce: every queued CacheNewTx is synced in one pass instead-            -- of one Redis round-trip sequence per transaction, so the inbox-            -- cannot grow without bound when Redis is slower than the mempool.-            more <- liftIO (atomically (drainNewTxs []))-            cacheWriterReact (CacheNewTxs (txid : more))-          _ -> cacheWriterReact x-    drainNewTxs acc-      | length acc >= 1000 = return (reverse acc)-      | otherwise = do-          empty <- mailboxEmptySTM inbox-          if empty-            then return (reverse acc)-            else-              receiveSTM inbox >>= \case-                CacheNewTx t -> drainNewTxs (t : acc)-                other -> do-                  requeueSTM other inbox-                  return (reverse acc)---- | Try to take a lock that expires after five minutes. Returns 'True' if taken.-lockIt :: (MonadIO m) => ByteString -> CacheX m Bool-lockIt l = do-  go >>= \case-    Right Redis.Ok -> do-      logDebug "Acquired lock"-      withMetrics $ \s -> incrementCounter s.lockAcquired 1-      return True-    Right Redis.Pong -> do-      logError "Unexpected pong when acquiring lock"-      withMetrics $ \s -> incrementCounter s.lockFailed 1-      return False-    Right (Redis.Status s) -> do-      logError $-        "Unexpected status acquiring lock: " <> cs s-      withMetrics $ \m -> incrementCounter m.lockFailed 1-      return False-    Left (Redis.Bulk Nothing) -> do-      logDebug "Lock already taken"-      withMetrics $ \s -> incrementCounter s.lockFailed 1-      return False-    Left e -> do-      logError "Error when trying to acquire lock"-      withMetrics $ \s -> incrementCounter s.lockFailed 1-      throwIO (RedisError e)-  where-    go = do-      conn <- asks (.redis)-      liftIO . Redis.runRedis conn $ do-        let opts =-              Redis.SetOpts-                { Redis.setSeconds = Just 300,-                  Redis.setMilliseconds = Nothing,-                  Redis.setUnixSeconds = Nothing,-                  Redis.setUnixMilliseconds = Nothing,-                  Redis.setCondition = Just Redis.Nx,-                  Redis.setKeepTTL = False-                }-        Redis.setOpts l "locked" opts---- | Extend a lock for another five minutes.-refreshLock :: (MonadIO m) => ByteString -> CacheX m ()-refreshLock l = void . runRedis $ do-  let opts =-        Redis.SetOpts-          { Redis.setSeconds = Just 300,-            Redis.setMilliseconds = Nothing,-            Redis.setUnixSeconds = Nothing,-            Redis.setUnixMilliseconds = Nothing,-            Redis.setCondition = Just Redis.Xx,-            Redis.setKeepTTL = False-          }-  Redis.setOpts l "locked" opts---- | Release a lock.-unlockIt :: (MonadIO m) => ByteString -> CacheX m ()-unlockIt l = void . runRedis . Redis.del $ NonEmpty.singleton l---- | Run an action holding a lock, refreshing it every two and a half minutes. Returns 'Nothing' if the lock is taken.-withLock ::-  (MonadUnliftIO m) =>-  ByteString ->-  CacheX m a ->-  CacheX m (Maybe a)-withLock l f =-  bracket (lockIt l) (`when` unlockIt l) $ \case-    True -> Just <$> go-    False -> return Nothing-  where-    go = withAsync refresh $ const f-    refresh = loopForever $ do-      threadDelay (150 * 1000 * 1000)-      refreshLock l---- | Run the action holding the lock, retrying with exponential backoff--- while another thread holds it. The delay between attempts grows from--- 0.2 s to at most 5 s, and after about 30 s of waiting it gives up and--- returns 'Nothing' instead of keeping the calling thread blocked forever.-withRetryLock ::-  (MonadUnliftIO m) =>-  ByteString ->-  CacheX m a ->-  CacheX m (Maybe a)-withRetryLock l f =-  go 0 (200 * 1000)-  where-    max_delay = 5 * 1000 * 1000-    max_wait = 30 * 1000 * 1000-    go waited n =-      withLock l f >>= \case-        Just x -> return (Just x)-        Nothing-          | waited >= max_wait -> return Nothing-          | otherwise -> do-              threadDelay n-              go (waited + n) (min max_delay (n * 2))---- | Evict the least recently used xpubs while Redis holds more than the maximum number of keys.-pruneDB ::-  (MonadUnliftIO m, StoreReadBase m) => CacheX m ()-pruneDB = void . withLock "lock" $ do-  x <- asks (.maxKeys)-  s <- runRedis Redis.dbsize-  logDebug "Pruning old xpubs"-  when (s > x) $-    void . delXPubKeys . map fst-      =<< runRedis (getFromSortedSet maxKey Nothing 0 (fromIntegral (s - x)))---- | Record that xpubs were just used.-touchKeys :: (MonadIO m) => [XPubSpec] -> CacheX m ()-touchKeys xpubs = do-  now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int-  runRedis $ redisTouchKeys now xpubs---- | Redis command to record that xpubs were used at a time.-redisTouchKeys :: (Monad f, RedisCtx m f, Real a) => a -> [XPubSpec] -> m (f ())-redisTouchKeys _ [] = return $ return ()-redisTouchKeys now xpubs =-  void <$> Redis.zadd maxKey (map ((realToFrac now,) . encode) xpubs)---- | Handle a message sent to the cache writer.-cacheWriterReact ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  CacheWriterMessage ->-  CacheX m ()-cacheWriterReact CacheNewBlock = do-  logDebug "Received new block event"-  pruneDB-  newBlockC-  syncMempoolC-cacheWriterReact (CacheNewTx txid) =-  cacheWriterReact (CacheNewTxs [txid])-cacheWriterReact (CacheNewTxs txids) = do-  logDebug $-    "Received " <> cs (show (length txids)) <> " new transaction event(s)"-  pruneDB-  syncNewTxC txids-cacheWriterReact (CacheSyncMempool l) = do-  logDebug "Received sync mempool event"-  pruneDB-  newBlockC-  syncMempoolC-  atomically $ l ()---- | Number of used addresses in a list of xpub balances.-lenNotNull :: [XPubBal] -> Int-lenNotNull = length . filter (not . nullBalance . (.balance))---- | Cache an xpub, if it has enough used addresses, the store is in sync, and no--- other thread is caching it already.-newXPubC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  XPubSpec ->-  [XPubBal] ->-  CacheX m ()-newXPubC xpub xbals =-  should_index >>= \i -> when i $-    bracket set_index unset_index $ \j -> when j $-      withTiming (.indexTime) $ do-        xpubtxt <- xpubText xpub-        logDebug $-          "Caching "-            <> xpubtxt-            <> ": "-            <> cs (show (length xbals))-            <> " addresses / "-            <> cs (show (lenNotNull xbals))-            <> " used"-        utxo <- lift $ xPubUnspents xpub xbals def-        logDebug $-          "Caching "-            <> xpubtxt-            <> ": "-            <> cs (show (length utxo))-            <> " utxos"-        xtxs <- lift $ xPubTxs xpub xbals def-        logDebug $-          "Caching "-            <> xpubtxt-            <> ": "-            <> cs (show (length xtxs))-            <> " txs"-        now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int-        runRedis $ do-          b <- redisTouchKeys now [xpub]-          c <- redisAddXPubBalances xpub xbals-          d <- redisAddXPubUnspents xpub (map op utxo)-          e <- redisAddXPubTxs xpub xtxs-          return $ b >> c >> d >> e >> return ()-        logDebug $ "Cached " <> xpubtxt-  where-    op XPubUnspent {unspent = u} = (u.outpoint, u.block)-    should_index =-      asks (.minAddrs) >>= \x ->-        if x <= lenNotNull xbals-          then inSync-          else return False-    key = idxPfx <> encode xpub-    opts =-      Redis.SetOpts-        { Redis.setSeconds = Just 600,-          Redis.setMilliseconds = Nothing,-          Redis.setUnixSeconds = Nothing,-          Redis.setUnixMilliseconds = Nothing,-          Redis.setCondition = Just Redis.Nx,-          Redis.setKeepTTL = False-        }-    red = Redis.setOpts key "1" opts-    unset_index y =-      when y . void . runRedis . Redis.del $ NonEmpty.singleton key-    set_index = do-      conn <- asks (.redis)-      liftIO (Redis.runRedis conn red) <&> isRight---- | Whether the best block of the store is the best header of the node.-inSync ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  CacheX m Bool-inSync =-  lift getBestBlock >>= \case-    Nothing -> return False-    Just bb -> do-      ch <- asks (.chain)-      cb <- liftIO $ chainGetBest ch-      return $ cb.height > 0 && headerHash cb.header == bb---- | Bring the cache up to date with the best block of the store, processing every--- block since the last one it processed, across reorganisations.-newBlockC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  CacheX m ()-newBlockC =-  inSync >>= \s -> when s . void . withLock "lock" $ do-    get_best_block_node >>= \case-      Nothing -> logError "No best block available"-      Just best_block_node ->-        cacheGetHead >>= \case-          Nothing -> do-            logInfo "Initializing best cache block"-            importBlockC $ headerHash best_block_node.header-          Just cache_head_hash ->-            get_block_node cache_head_hash >>= \case-              Nothing -> do-                logError $-                  "Could not get best cache block: "-                    <> blockHashToHex cache_head_hash-              Just cache_head_node -> do-                blocks <- get_blocks cache_head_node best_block_node-                mapM_ importBlockC blocks-  where-    get_best_block_node =-      lift getBestBlock >>= \case-        Nothing -> return Nothing-        Just best_block_hash -> get_block_node best_block_hash-    get_block_node block_hash = do-      ch <- asks (.chain)-      liftIO $ chainGetBlock ch block_hash-    get_blocks left_node right_node = do-      ch <- asks (.chain)-      split_node <- liftIO $ chainGetSplitBlock ch left_node right_node-      let split_node_hash = headerHash split_node.header-          right_node_hash = headerHash right_node.header-      if split_node_hash == right_node_hash-        then return []-        else do-          let fork_height = split_node.height + 1-          left_parents <- liftIO $ chainGetParents ch fork_height left_node-          right_parents <- liftIO $ chainGetParents ch fork_height right_node-          let blocks = reverse left_parents <> right_parents <> pure right_node-          return $ map (headerHash . (.header)) blocks---- | Update cached xpubs with the transactions of a block.-importBlockC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  BlockHash ->-  CacheX m ()-importBlockC bh =-  lift (getBlock bh) >>= \case-    Just bd -> do-      let ths = bd.txs-      tds <- sortTxData . catMaybes <$> mapM (lift . getTxData) ths-      logDebug $-        "Importing "-          <> cs (show (length tds))-          <> " transactions from block "-          <> blockHashToHex bh-      importMultiTxC tds-      logDebug $-        "Done importing "-          <> cs (show (length tds))-          <> " transactions from block "-          <> blockHashToHex bh-      cacheSetHead bd-    Nothing -> do-      logError $-        "Could not get block: "-          <> blockHashToHex bh-      throwIO . LogicError . cs $-        "Could not get block: "-          <> blockHashToHex bh---- | Update cached xpubs with transactions: their transaction sets, unspent--- outputs, balances and the mempool. Addresses past the gap are derived and--- added when new addresses get used.-importMultiTxC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  [TxData] ->-  CacheX m ()-importMultiTxC txs = do-  net <- lift getNetwork-  ctx <- lift getCtx-  logDebug $ "Processing " <> cs (show (length txs)) <> " txs"-  logDebug $-    "Getting address information for "-      <> cs (show (length (alladdrs net ctx)))-      <> " addresses"-  addrmap <- getaddrmap net ctx-  let addrs = HashMap.keys addrmap-  logDebug $-    "Getting balances for "-      <> cs (show (HashMap.size addrmap))-      <> " addresses"-  balmap <- getbalances addrs-  logDebug $-    "Getting unspent data for "-      <> cs (show (length (allops net ctx)))-      <> " outputs"-  unspentmap <- getunspents net ctx-  gap <- lift getMaxGap-  now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int-  let xpubs = allxpubsls addrmap-  forM_ (zip [(1 :: Int) ..] xpubs) $ \(i, xpub) -> do-    xpubtxt <- xpubText xpub-    logDebug $-      "Affected xpub "-        <> cs (show i)-        <> "/"-        <> cs (show (length xpubs))-        <> ": "-        <> xpubtxt-  addrs' <- do-    logDebug $-      "Getting xpub balances for "-        <> cs (show (length xpubs))-        <> " xpubs"-    xmap <- getxbals xpubs-    let addrmap' = faddrmap (HashMap.keysSet xmap) addrmap-    logDebug "Starting Redis import pipeline"-    runRedis $ do-      x <- redisImportMultiTx net ctx addrmap' unspentmap txs-      y <- redisUpdateBalances addrmap' balmap-      z <- redisTouchKeys now (HashMap.keys xmap)-      return $ x >> y >> z >> return ()-    logDebug "Completed Redis pipeline"-    return $ getNewAddrs ctx gap xmap (HashMap.elems addrmap')-  cacheAddAddresses addrs'-  where-    alladdrsls net ctx = HashSet.toList (alladdrs net ctx)-    faddrmap xmap = HashMap.filter (\a -> a.spec `elem` xmap)-    getaddrmap net ctx =-      HashMap.fromList-        . catMaybes-        . zipWith (\a -> fmap (a,)) (alladdrsls net ctx)-        <$> cacheGetAddrsInfo (alladdrsls net ctx)-    getunspents net ctx =-      HashMap.fromList-        . catMaybes-        . zipWith (\p -> fmap (p,)) (allops net ctx)-        <$> lift (mapM getUnspent (allops net ctx))-    getbalances addrs =-      HashMap.fromList . zip addrs <$> mapM (lift . getDefaultBalance) addrs-    getxbals xpubs = do-      bals <- runRedis . fmap sequence . forM xpubs $ \xpub -> do-        bs <- redisGetXPubBalances xpub-        return $ (,) xpub <$> bs-      return $ HashMap.filter (not . null) (HashMap.fromList bals)-    allops net ctx =-      map snd $-        concatMap (txInputs net ctx) txs-          <> concatMap (txOutputs net ctx) txs-    alladdrs net ctx =-      HashSet.fromList $-        map fst $-          concatMap (txInputs net ctx) txs-            <> concatMap (txOutputs net ctx) txs-    allxpubsls = HashSet.toList . allxpubs-    allxpubs =-      HashSet.fromList . map (.spec) . HashMap.elems---- | Redis commands to add or remove transactions and unspent outputs of cached xpubs.-redisImportMultiTx ::-  (Monad f, RedisCtx m f) =>-  Network ->-  Ctx ->-  HashMap Address AddressXPub ->-  HashMap OutPoint Unspent ->-  [TxData] ->-  m (f ())-redisImportMultiTx net ctx addrmap unspentmap tds = do-  xs <- mapM importtxentries tds-  return $ sequence_ xs-  where-    uns p i =-      case HashMap.lookup p unspentmap of-        Just u ->-          redisAddXPubUnspents i.spec [(p, u.block)]-        Nothing -> redisRemXPubUnspents i.spec [p]-    addtx tx a p =-      case HashMap.lookup a addrmap of-        Just i -> do-          let tr =-                TxRef-                  { txid = txHash tx.tx,-                    block = tx.block-                  }-          x <- redisAddXPubTxs i.spec [tr]-          y <- uns p i-          return $ x >> y >> return ()-        Nothing -> return (pure ())-    remtx tx a p =-      case HashMap.lookup a addrmap of-        Just i -> do-          x <- redisRemXPubTxs i.spec [txHash tx.tx]-          y <- uns p i-          return $ x >> y >> return ()-        Nothing -> return (pure ())-    importtxentries td =-      if td.deleted-        then do-          x <--            mapM-              (uncurry (remtx td))-              (txaddrops td)-          y <- redisRemFromMempool [txHash td.tx]-          return $ sequence_ x >> void y-        else do-          a <--            sequence-              <$> mapM-                (uncurry (addtx td))-                (txaddrops td)-          b <--            case td.block of-              b@MemRef {} ->-                let tr =-                      TxRef-                        { txid = txHash td.tx,-                          block = b-                        }-                 in redisAddToMempool [tr]-              _ -> redisRemFromMempool [txHash td.tx]-          return $ a >> b >> return ()-    txaddrops td = txInputs net ctx td <> txOutputs net ctx td---- | Redis commands to update the balances of cached addresses.-redisUpdateBalances ::-  (Monad f, RedisCtx m f) =>-  HashMap Address AddressXPub ->-  HashMap Address Balance ->-  m (f ())-redisUpdateBalances addrmap balmap =-  fmap (fmap mconcat . sequence) . forM (HashMap.keys addrmap) $ \a ->-    case (HashMap.lookup a addrmap, HashMap.lookup a balmap) of-      (Just ainfo, Just bal) ->-        redisAddXPubBalances ainfo.spec [xpubbal ainfo bal]-      _ -> return (pure ())-  where-    xpubbal ainfo bal =-      XPubBal {path = ainfo.path, balance = bal}---- | Add newly derived addresses to their cached xpubs, with their balances,--- transactions and unspent outputs, and keep deriving while they are used.-cacheAddAddresses ::-  (StoreReadExtra m, MonadUnliftIO m) =>-  [(Address, AddressXPub)] ->-  CacheX m ()-cacheAddAddresses [] = logDebug "No further addresses to add"-cacheAddAddresses addrs = do-  ctx <- lift getCtx-  logDebug $-    "Adding " <> cs (show (length addrs)) <> " new generated addresses"-  logDebug "Getting balances"-  balmap <- HashMap.fromListWith (<>) <$> mapM (uncurry getbal) addrs-  logDebug "Getting unspent outputs"-  utxomap <- HashMap.fromListWith (<>) <$> mapM (uncurry getutxo) addrs-  logDebug "Getting transactions"-  txmap <- HashMap.fromListWith (<>) <$> mapM (uncurry gettxmap) addrs-  logDebug "Running Redis pipeline"-  runRedis $ do-    a <- forM (HashMap.toList balmap) (uncurry redisAddXPubBalances)-    b <- forM (HashMap.toList utxomap) (uncurry redisAddXPubUnspents)-    c <- forM (HashMap.toList txmap) (uncurry redisAddXPubTxs)-    return $ sequence_ a >> sequence_ b >> sequence_ c-  logDebug "Completed Redis pipeline"-  let xpubs =-        HashSet.toList-          . HashSet.fromList-          . map (.spec)-          $ Map.elems amap-  logDebug "Getting xpub balances"-  xmap <- getbals xpubs-  gap <- lift getMaxGap-  let notnulls = getnotnull balmap-      addrs' = getNewAddrs ctx gap xmap notnulls-  cacheAddAddresses addrs'-  where-    getbals xpubs = runRedis $ do-      bs <- sequence <$> forM xpubs redisGetXPubBalances-      return $-        HashMap.filter (not . null)-          . HashMap.fromList-          . zip xpubs-          <$> bs-    amap = Map.fromList addrs-    getnotnull =-      let f xpub =-            map $ \bal ->-              AddressXPub-                { spec = xpub,-                  path = bal.path-                }-          g = filter (not . nullBalance . (.balance))-       in concatMap (uncurry f) . HashMap.toList . HashMap.map g-    getbal a i =-      let f b =-            ( i.spec,-              [XPubBal {balance = b, path = i.path}]-            )-       in f <$> lift (getDefaultBalance a)-    getutxo a i =-      let f us =-            ( i.spec,-              map (\u -> (u.outpoint, u.block)) us-            )-       in f <$> lift (getAddressUnspents a def)-    gettxmap a i =-      let f ts = (i.spec, ts)-       in f <$> lift (getAddressTxs a def)---- | Addresses to derive and add to cached xpubs to keep the gap after the used ones.-getNewAddrs ::-  Ctx ->-  KeyIndex ->-  HashMap XPubSpec [XPubBal] ->-  [AddressXPub] ->-  [(Address, AddressXPub)]-getNewAddrs ctx gap xpubs =-  concatMap $ \a ->-    case HashMap.lookup a.spec xpubs of-      Nothing -> []-      Just bals -> addrsToAdd ctx gap bals a---- | Update cached xpubs with new or deleted transactions, if in sync.-syncNewTxC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  [TxHash] ->-  CacheX m ()-syncNewTxC ths =-  inSync >>= \s -> when s . void . withLock "lock" $ do-    txs <- catMaybes <$> mapM (lift . getTxData) ths-    unless (null txs) $ do-      forM_ txs $ \tx ->-        logDebug $-          "Synchronizing transaction: " <> txHashToHex (txHash tx.tx)-      importMultiTxC txs---- | Update cached xpubs with every mempool difference between the cache and the store, if in sync.-syncMempoolC ::-  (MonadUnliftIO m, StoreReadExtra m) =>-  CacheX m ()-syncMempoolC =-  inSync >>= \s -> when s . void . withLock "lock" $ do-    nodepool <- HashSet.fromList . map snd <$> lift getMempool-    cachepool <- HashSet.fromList . map snd <$> cacheGetMempool-    let diff1 = HashSet.difference nodepool cachepool-    let diff2 = HashSet.difference cachepool nodepool-    let diffset = diff1 <> diff2-    let tids = HashSet.toList diffset-    txs <- catMaybes <$> mapM (lift . getTxData) tids-    unless (null txs) $ do-      logDebug $-        "Synchronizing " <> cs (show (length txs)) <> " mempool transactions"-      importMultiTxC txs---- | Mempool as seen by the cache, newest first.-cacheGetMempool :: (MonadIO m) => CacheX m [(UnixTime, TxHash)]-cacheGetMempool = runRedis redisGetMempool---- | Last block processed by the cache.-cacheGetHead :: (MonadIO m) => CacheX m (Maybe BlockHash)-cacheGetHead = runRedis redisGetHead---- | Set the last block processed by the cache.-cacheSetHead :: (MonadIO m) => BlockData -> CacheX m ()-cacheSetHead bd = do-  logDebug $ "Cache head set to: " <> blockHashToHex bh-  void $ runRedis (redisSetHead bh)-  withMetrics $ \s -> setGauge s.height (fromIntegral bd.height)-  where-    bh = headerHash bd.header---- | Xpub and path of each address, or 'Nothing' if the address is not cached.-cacheGetAddrsInfo ::-  (MonadIO m) => [Address] -> CacheX m [Maybe AddressXPub]-cacheGetAddrsInfo as = runRedis (redisGetAddrsInfo as)---- | Redis command to add transactions to the mempool set.-redisAddToMempool :: (Applicative f, RedisCtx m f) => [TxRef] -> m (f Integer)-redisAddToMempool [] = return (pure 0)-redisAddToMempool btxs =-  zadd mempoolSetKey $-    map-      (\btx -> (blockRefScore btx.block, encode btx.txid))-      btxs---- | Redis command to remove transactions from the mempool set.-redisRemFromMempool ::-  (Applicative f, RedisCtx m f) => [TxHash] -> m (f Integer)-redisRemFromMempool xs =-  case NonEmpty.nonEmpty xs of-    Nothing -> return (pure 0)-    Just xs' -> zrem mempoolSetKey $ NonEmpty.map encode xs'---- | Redis command to record the xpub and path of an address.-redisSetAddrInfo ::-  (Functor f, RedisCtx m f) => Address -> AddressXPub -> m (f ())-redisSetAddrInfo a i = void <$> Redis.set (addrPfx <> encode a) (encode i)---- | Remove xpubs from the cache. Returns the number of keys deleted.-cacheDelXPubs ::-  (MonadIO m, StoreReadBase m) =>-  [XPubSpec] ->-  CacheT m Integer-cacheDelXPubs xpubs = ReaderT $ \case-  Just cache -> runReaderT (delXPubKeys xpubs) cache-  Nothing -> return 0---- | Remove xpubs from the cache. Returns the number of keys deleted.-delXPubKeys ::-  (MonadIO m, StoreReadBase m) =>-  [XPubSpec] ->-  CacheX m Integer-delXPubKeys [] = return 0-delXPubKeys xpubs = do-  forM_ xpubs $ \x -> do-    xtxt <- xpubText x-    logDebug $ "Deleting xpub: " <> xtxt-  xbals <--    runRedis . fmap sequence . forM xpubs $ \xpub -> do-      bs <- redisGetXPubBalances xpub-      return $ (xpub,) <$> bs-  runRedis $ fmap sum . sequence <$> forM xbals (uncurry redisDelXPubKeys)---- | Redis commands to remove an xpub and its addresses from the cache.-redisDelXPubKeys ::-  (Monad f, RedisCtx m f) => XPubSpec -> [XPubBal] -> m (f Integer)-redisDelXPubKeys xpub bals = go $ map (.balance.address) bals-  where-    go addrs = do-      addrcount <--        case NonEmpty.nonEmpty addrs of-          Nothing -> return (pure 0)-          Just addrs' -> Redis.del $ NonEmpty.map ((addrPfx <>) . encode) addrs'-      txsetcount <- Redis.del $ NonEmpty.singleton (txSetPfx <> encode xpub)-      utxocount <- Redis.del $ NonEmpty.singleton (utxoPfx <> encode xpub)-      balcount <- Redis.del $ NonEmpty.singleton (balancesPfx <> encode xpub)-      x <- Redis.zrem maxKey $ NonEmpty.singleton (encode xpub)-      return $ do-        _ <- x-        addrs' <- addrcount-        txset' <- txsetcount-        utxo' <- utxocount-        bal' <- balcount-        return $ addrs' + txset' + utxo' + bal'---- | Redis command to add transactions to an xpub.-redisAddXPubTxs ::-  (Applicative f, RedisCtx m f) => XPubSpec -> [TxRef] -> m (f Integer)-redisAddXPubTxs _ [] = return (pure 0)-redisAddXPubTxs xpub btxs =-  zadd (txSetPfx <> encode xpub) $-    map (\t -> (blockRefScore t.block, encode t.txid)) btxs---- | Redis command to remove transactions from an xpub.-redisRemXPubTxs ::-  (Applicative f, RedisCtx m f) => XPubSpec -> [TxHash] -> m (f Integer)-redisRemXPubTxs xpub txhs =-  case NonEmpty.nonEmpty txhs of-    Nothing -> return (pure 0)-    Just txhs' -> zrem (txSetPfx <> encode xpub) (NonEmpty.map encode txhs')---- | Redis command to add unspent outputs to an xpub.-redisAddXPubUnspents ::-  (Applicative f, RedisCtx m f) =>-  XPubSpec ->-  [(OutPoint, BlockRef)] ->-  m (f Integer)-redisAddXPubUnspents _ [] =-  return (pure 0)-redisAddXPubUnspents xpub utxo =-  zadd (utxoPfx <> encode xpub) $-    map (\(p, r) -> (blockRefScore r, encode p)) utxo---- | Redis command to remove unspent outputs from an xpub.-redisRemXPubUnspents ::-  (Applicative f, RedisCtx m f) => XPubSpec -> [OutPoint] -> m (f Integer)-redisRemXPubUnspents _ [] =-  return (pure 0)-redisRemXPubUnspents xpub ops =-  case NonEmpty.nonEmpty ops of-    Nothing -> return (pure 0)-    Just ops' -> zrem (utxoPfx <> encode xpub) (NonEmpty.map encode ops')---- | Redis commands to set balances of an xpub and record its addresses.-redisAddXPubBalances ::-  (Monad f, RedisCtx m f) => XPubSpec -> [XPubBal] -> m (f ())-redisAddXPubBalances xpub bals =-  case NonEmpty.nonEmpty bals of-    Nothing -> return (pure ())-    Just bals' -> do-      let bs = NonEmpty.map (\b -> (encode b.path, encode b.balance)) bals'-      Redis.hset (balancesPfx <> encode xpub) bs-      ys <- forM bals' $ \b ->-        redisSetAddrInfo-          b.balance.address-          AddressXPub-            { spec = xpub,-              path = b.path-            }-      return $ sequence_ ys---- | Redis command to set the last block processed by the cache.-redisSetHead :: (RedisCtx m f) => BlockHash -> m (f Redis.Status)-redisSetHead bh = Redis.set bestBlockKey (encode bh)---- | Redis commands to read the xpub and path of addresses.-redisGetAddrsInfo ::-  (Monad f, RedisCtx m f) => [Address] -> m (f [Maybe AddressXPub])-redisGetAddrsInfo [] = return (pure [])-redisGetAddrsInfo as = do-  is <- mapM (\a -> Redis.get (addrPfx <> encode a)) as-  return $ do-    is' <- sequence is-    return $ map (eitherToMaybe . decode =<<) is'---- | Addresses to derive after a newly used address, so that the gap of unused--- addresses after the last used one on its chain is kept.-addrsToAdd ::-  Ctx ->-  KeyIndex ->-  [XPubBal] ->-  AddressXPub ->-  [(Address, AddressXPub)]-addrsToAdd ctx gap xbals addrinfo-  | null fbals =-      []-  | not haschange =-      zipWith f addrs list-        <> zipWith f changeaddrs changelist-  | otherwise =-      zipWith f addrs list-  where-    haschange = any ((== 1) . (!! 0) . (.path)) xbals-    f a p = (a, AddressXPub {spec = xpub, path = p})-    dchain = addrinfo.path !! 0-    fbals = filter ((== dchain) . (!! 0) . (.path)) xbals-    maxidx = maximum (map ((!! 1) . (.path)) fbals)-    xpub = addrinfo.spec-    aidx = addrinfo.path !! 1-    ixs =-      if gap > maxidx - aidx-        then [maxidx + 1 .. aidx + gap]-        else []-    paths = map (Deriv :/ dchain :/) ixs-    keys = map (\p -> derivePubPath ctx p xpub.key)-    list = map pathToList paths-    xpubf = xPubAddrFunction ctx xpub.deriv-    addrs = map xpubf (keys paths)-    changepaths = map (Deriv :/ 1 :/) [0 .. gap - 1]-    changeaddrs = map xpubf (keys changepaths)-    changelist = map pathToList changepaths---- | Sort transactions so that each comes after those it spends.-sortTxData :: [TxData] -> [TxData]-sortTxData tds =-  let txm = Map.fromList (map (\d -> (txHash d.tx, d)) tds)-      ths = map (txHash . snd) (sortTxs (map (.tx) tds))-   in mapMaybe (`Map.lookup` txm) ths---- | Addresses and outpoints of the outputs spent by a transaction.-txInputs :: Network -> Ctx -> TxData -> [(Address, OutPoint)]-txInputs net ctx td =-  let is = td.tx.inputs-      ps = I.toAscList td.prevs-      as = map (scriptToAddressBS net ctx . (.script) . snd) ps-      f (Right a) i = Just (a, i.outpoint)-      f (Left _) _ = Nothing-   in catMaybes (zipWith f as is)---- | Addresses and outpoints of the outputs of a transaction.-txOutputs :: Network -> Ctx -> TxData -> [(Address, OutPoint)]-txOutputs net ctx td =-  let ps =-        zipWith-          ( \i _ ->-              OutPoint-                { hash = txHash td.tx,-                  index = i-                }-          )-          [0 ..]-          td.tx.outputs-      as = map (scriptToAddressBS net ctx . (.script)) td.tx.outputs-      f (Right a) p = Just (a, p)-      f (Left _) _ = Nothing-   in catMaybes (zipWith f as ps)---- | Redis command to read the last block processed by the cache.-redisGetHead :: (Functor f, RedisCtx m f) => m (f (Maybe BlockHash))-redisGetHead = do-  x <- Redis.get bestBlockKey-  return $ (eitherToMaybe . decode =<<) <$> x---- | Redis command to read the mempool set, newest first.-redisGetMempool :: (Applicative f, RedisCtx m f) => m (f [(UnixTime, TxHash)])-redisGetMempool = do-  xs <- getFromSortedSet mempoolSetKey Nothing 0 0-  return $ map (uncurry f) <$> xs-  where-    f t s = ((scoreBlockRef s).timestamp, t)---- | Text form of an xpub for logs, with a suffix for its address type.-xpubText ::-  (StoreReadBase m) =>-  XPubSpec ->-  CacheX m Text-xpubText xpub = do-  net <- lift getNetwork-  let suffix = case xpub.deriv of-        DeriveNormal -> ""-        DeriveP2SH -> "/p2sh"-        DeriveP2WPKH -> "/p2wpkh"-  ctx <- lift getCtx-  return . cs $ suffix <> xPubExport net ctx xpub.key---- | Tell the cache writer that a new block was imported.-cacheNewBlock :: (MonadIO m) => CacheWriter -> m ()-cacheNewBlock = send CacheNewBlock---- | Tell the cache writer that a mempool transaction was added or removed.-cacheNewTx :: (MonadIO m) => TxHash -> CacheWriter -> m ()-cacheNewTx = send . CacheNewTx---- | Ask the cache writer to sync the mempool, and wait until it is done.-cacheSyncMempool :: (MonadIO m) => CacheWriter -> m ()-cacheSyncMempool = query CacheSyncMempool+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}++-- |+-- Module      : Haskoin.Store.Cache+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Redis cache of extended public keys (xpubs), shared by several stores.+--+-- An xpub with many used addresses is slow to answer from the database: every+-- address has to be derived and read. Wallets of exchanges and payment+-- processors are like that, and are asked about all the time. The cache keeps+-- everything that the web server needs to answer about such an xpub in Redis:+-- the balances of its addresses up to the gap, its transactions, its unspent+-- outputs, and the bodies of its transactions with who spends their outputs.+--+-- = One source of truth+--+-- The data of a cached xpub always comes from the database, read with the+-- same functions that answer uncached queries+-- ("Haskoin.Store.Database.Query"), and is never computed from what the+-- cache guesses a change did, as the cache that this one replaced did, and+-- drifted. Whenever a block or a mempool transaction touches one of its+-- addresses, or a reorganisation changes one of its transactions, the store+-- that saw it reads from the database what that change affects, the+-- transactions and the outputs they create and spend, and writes the+-- differences in one atomic step ('cacheRefresh'). It reads the whole xpub+-- instead when it caches it, and whenever it cannot be sure that the entry+-- has every earlier change: checks against the balances of every address of+-- the xpub, and a full reload every ten minutes, keep a missed change from+-- staying. Reloads of one xpub are coalesced, so a burst of payments costs at+-- most one reload in progress and one waiting.+--+-- Everything a response needs is in Redis, including the bodies of the+-- transactions: a store that has not yet seen a transaction of a cached xpub+-- still serves it in full. Bodies are kept once per transaction and shared by+-- the xpubs that refer to them; the last xpub to let go of one deletes it.+-- Bodies are stored without the spenders of their outputs, which change+-- without touching the xpub (an output paying someone else is spent by a+-- transaction that has nothing to do with the xpub). The spenders are in a+-- separate hash per transaction, kept up to date by every store as it imports+-- transactions, and filled in when a body is read.+--+-- = Several stores, one Redis+--+-- Stores index the same chain at slightly different times, and see mempool+-- transactions in different orders. Three rules keep them from getting in+-- each other's way:+--+-- * __Each change is applied once.__ A store claims each change it sees, per+--   cached xpub that it touches (@ev:\<event\>:\<id\>@). The first store to+--   claim it reloads the xpub and marks the claim done; the others skip it. A+--   store that is catching up finds its events done and does no work, and a+--   store whose database does not have the best header ignores events+--   altogether.+--+-- * __No store writes what knows less than Redis.__ A store does not replace+--   an entry stamped with a higher block than its own best block, nor one+--   with a transaction that its database has never seen; it gives the change+--   up for a store that knows more. So stores never undo each other's work.+--+-- * __No entry refers to what Redis does not have.__ An entry is written in+--   one Lua script together with the bodies it needs, and the script writes+--   nothing if a body would be missing. A store that is behind can serve a+--   cached xpub in full, transactions it has not imported included. Bodies+--   that Redis already has are rewritten just before, in small scripts, so+--   that a large block does not hold Redis for long; a body may so be a+--   moment newer than an entry that has it, never older.+--+-- The first two rules are the pure decisions of+-- "Haskoin.Store.Cache.Protocol", where a model test checks them with several+-- stores catching up in random interleavings.+--+-- A lock per xpub keeps two stores from reloading it at the same time, and+-- every change of several keys of an entry is a Lua script, so readers never+-- see half of one. Stores that answer from Redis serve the same data; stores whose+-- databases disagree can still give different answers for xpubs that are not+-- cached.+--+-- = Keys+--+-- Every key starts with @hs2:\<network\>:@, so this cache can share a Redis with+-- the cache of older versions of the store, whose keys have no such prefix,+-- and with stores of other networks.+--+-- * @x:\<id\>:meta@: hash with the serialized xpub (@spec@), the height of+--   the best block it was read at (@height@), the format version (@ver@,+--   now 3), the generation of the entry (@gen@), which every reload+--   increments, and the time of its last full reload in milliseconds+--   (@full@).+--   @\<id\>@ is the hexadecimal of the first 16 bytes of the SHA-256 of the+--   serialized xpub.+-- * @x:\<id\>:bal@: balances of the addresses up to the gap, serialized.+-- * @x:\<id\>:txs@: sorted set of transaction hashes, scored by 'blockRefScore'.+-- * @x:\<id\>:utxo@: sorted set of serialized unspent outputs, scored the same.+-- * @x:\<id\>:uop@: hash of serialized outpoint to its member of @utxo@, so+--   that an output can be removed by its outpoint alone, though its member+--   changes with its block. Entries of version 2 have none; their next full+--   reload builds it.+-- * @x:\<id\>:addrs@: set of the addresses of the balances.+-- * @a:\<address\>@: set of the ids of the cached xpubs with this address.+-- * @tx:\<txid\>@: transaction body, serialized 'TxData' without spenders.+-- * @sp:\<txid\>@: hash of output index to spender.+-- * @txref:\<txid\>@: set of the ids of the cached xpubs with this transaction.+-- * @lru@: sorted set of ids by time of last use, for eviction.+-- * @lock:\<id\>@: lock held while an xpub is reloaded.+-- * @ev:\<event\>:\<id\>@: claim of a change of a cached xpub: @busy@ while a+--   store applies it, for two minutes at most, then @done@ for an hour.+module Haskoin.Store.Cache+  ( -- * Running the cache+    Cache,+    CacheConfig (..),+    withCache,+    cacheAlive,++    -- * Reading+    cacheXPubBals,+    cacheXPubTxs,+    cacheXPubTxCount,+    cacheXPubUnspents,+    cacheTxData,+    cacheIsCached,+    CacheCounters (..),+    cacheCounters,++    -- * Changing+    cacheRequest,+    Reload (..),+    cacheRefresh,+    cacheDelete,+    cacheEvict,++    -- * Internals++    -- | Exported for tests.+    cachePrefix,+    xpubId,+    blockRefScore,+    scoreBlockRef,+    fullReloadEvery,+    fullReloadAfter,+  )+where++import Control.Monad (forM, forM_, join, unless, void, when)+import Data.Bits (shift, (.&.), (.|.))+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Char8 qualified as C8+import Data.Either (rights)+import Data.FileEmbed (embedFile, makeRelativeToProject)+import Data.Functor ((<&>))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict qualified as IntMap+import Data.List (nub)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, maybeToList)+import Data.Serialize (decode, encode)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word32, Word64)+import Database.Redis (Connection, Redis, Reply)+import Database.Redis qualified as R+import Haskoin+  ( Address,+    BlockHash,+    BlockHeader (..),+    BlockNode (..),+    Network (..),+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    encodeHex,+    headerHash,+    scriptToAddressBS,+    sha256,+    txHash,+  )+import Haskoin.Node (Chain, LogLevel (..), Logger, chainGetBest, logAt)+import Haskoin.Store.Cache.Protocol+  ( Claim (..),+    OnClaim (..),+    OnReload (..),+    Reload (..),+    afterClaim,+    afterReload,+    mayReplace,+  )+import Haskoin.Store.Common (Limits (..), Start (..), StoreEvent (..), loopForever)+import Haskoin.Store.Data+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader (DatabaseReader (..))+import NQE (Inbox, Publisher, receive, withSubscription)+import System.Metrics.StatsD+  ( StatCounter,+    StatTiming,+    Stats,+    addTiming,+    incrementCounter,+    newStatCounter,+    newStatTiming,+  )+import System.Random (randomIO)+import Text.Read (readMaybe)+import UnliftIO+  ( IORef,+    TVar,+    atomicModifyIORef',+    atomically,+    checkSTM,+    handleAny,+    link,+    modifyTVar',+    newIORef,+    newTVarIO,+    pooledForConcurrentlyN_,+    readIORef,+    readTVar,+    timeout,+    tryAny,+    withAsync,+    writeIORef,+    writeTVar,+  )+import UnliftIO.Concurrent (forkIO, threadDelay)++-- | Settings of the cache.+data CacheConfig = CacheConfig+  { -- | Redis URL, such as @redis:\/\/localhost:6379@; empty for the default+    url :: !String,+    -- | cache xpubs with at least this many used addresses+    minAddrs :: !Int,+    -- | or with at least this many transactions in their addresses+    minTxs :: !Int,+    -- | evict the least recently used xpubs past this many+    maxXPubs :: !Int,+    -- | StatsD client, to report the metrics of the cache+    stats :: !(Maybe Stats)+  }++-- | StatsD metrics of the cache.+data CacheMetrics = CacheMetrics+  { -- | xpubs answered from the cache (@cache.hits@)+    hits :: !StatCounter,+    -- | xpubs asked about that were not cached (@cache.misses@)+    misses :: !StatCounter,+    -- | xpubs read again from the database and written (@cache.reloads@)+    reloads :: !StatCounter,+    -- | reloads refused because the cache had newer data (@cache.stale@)+    stale :: !StatCounter,+    -- | xpubs evicted (@cache.evictions@)+    evictions :: !StatCounter,+    -- | reloads that read the whole xpub rather than what changed+    -- (@cache.full_reloads@)+    fullReloads :: !StatCounter,+    -- | milliseconds to reload an xpub (@cache.reload@)+    reloadTime :: !StatTiming+  }++-- | Register the metrics of the cache.+newCacheMetrics :: Stats -> IO CacheMetrics+newCacheMetrics s = do+  hits <- c "hits"+  misses <- c "misses"+  reloads <- c "reloads"+  stale <- c "stale"+  evictions <- c "evictions"+  fullReloads <- c "full_reloads"+  reloadTime <- newStatTiming s "cache.reload" 10+  return CacheMetrics {..}+  where+    c x = newStatCounter s ("cache." <> x) 10++-- | Read the database through a view of it as it is now: the mempool is+-- kept apart from the chain and laid over it when read, and the two have to+-- be read as they were at one moment+-- ("Haskoin.Store.Database.Query.Layered"). Transactions that left the+-- mempool unconfirmed are among what these reads give, marked as deleted:+-- the cache has to hear of them to take them out.+viewed :: Cache -> (L.View -> IO a) -> IO a+viewed c = L.withView c.db++-- | A running cache.+data Cache = Cache+  { redis :: !Connection,+    prefix :: !ByteString,+    db :: !DatabaseReader,+    chain :: !Chain,+    minAddrs :: !Int,+    minTxs :: !Int,+    maxXPubs :: !Int,+    logger :: !Logger,+    metrics :: !(Maybe CacheMetrics),+    -- | what this store did to the cache since it started+    counters :: !(IORef CacheCounters),+    -- | work for the worker, newest first+    jobs :: !(TVar [Job]),+    -- | when this store last marked each xpub as used, by id, in+    -- milliseconds; see 'touchEvery'+    touched :: !(IORef (HashMap ByteString Integer)),+    -- | transactions that this store deleted since it started, as far as+    -- the cache heard of them, up to a hundred thousand. The database+    -- keeps nothing of a deleted transaction, so without this one that+    -- Redis has and the database does not could as well be one that this+    -- store has not seen yet, which an entry must not lose.+    lost :: !(IORef (HashSet TxHash))+  }++-- | What a store did to the cache since it started.+data CacheCounters = CacheCounters+  { -- | xpubs read again from the database and written+    reloads :: !Int,+    -- | reloads given up because Redis knew more+    behind :: !Int,+    -- | reloads that read the whole xpub rather than what changed+    fullReloads :: !Int+  }+  deriving (Eq, Show)++-- | What this store did to the cache since it started.+cacheCounters :: Cache -> IO CacheCounters+cacheCounters c = readIORef c.counters++-- | Work for the worker of the cache.+data Job+  = -- | an event of the store, which may change cached xpubs+    JobEvent !StoreEvent+  | -- | cache an xpub that was asked about+    JobCache !XPubSpec+  | -- | try again to claim changes of an xpub that another store was busy+    -- with: the transactions that changed it, and the claims+    JobClaim !XPubSpec !(HashSet TxHash) ![ByteString]+  | -- | reload an xpub for changes whose claims this store holds+    JobHeld !XPubSpec !(HashSet TxHash) ![ByteString]++-- | Connect to Redis and run the process that keeps the cache up to date with+-- the events of the store, while an action runs.+withCache ::+  CacheConfig ->+  DatabaseReader ->+  Chain ->+  Publisher StoreEvent ->+  Logger ->+  (Cache -> IO a) ->+  IO a+withCache cfg db chain pub logger action = do+  info <-+    if null cfg.url+      then return R.defaultConnectInfo+      else either fail return (R.parseConnectInfo cfg.url)+  conn <- R.checkedConnect info+  jobs <- newTVarIO []+  counters <- newIORef (CacheCounters 0 0 0)+  touched <- newIORef HashMap.empty+  lost <- newIORef HashSet.empty+  metrics <- mapM newCacheMetrics cfg.stats+  let cache =+        Cache+          { redis = conn,+            prefix = cachePrefix db.net,+            db = db,+            chain = chain,+            minAddrs = cfg.minAddrs,+            minTxs = cfg.minTxs,+            maxXPubs = cfg.maxXPubs,+            logger = logger,+            metrics = metrics,+            counters = counters,+            jobs = jobs,+            touched = touched,+            lost = lost+          }+  withSubscription pub $ \events ->+    withAsync (listener cache events) $ \a1 ->+      withAsync (worker cache) $ \a2 -> do+        link a1+        link a2+        action cache++-- | Prefix of every key of the cache for a network.+cachePrefix :: Network -> ByteString+cachePrefix net = "hs2:" <> C8.pack net.name <> ":"++-- | Identifier of an xpub in the keys.+xpubId :: XPubSpec -> ByteString+xpubId = cs . encodeHex . BS.take 16 . encode . sha256 . encode++xkey :: Cache -> XPubSpec -> ByteString -> ByteString+xkey c x suffix = c.prefix <> "x:" <> xpubId x <> ":" <> suffix++txKey :: Cache -> TxHash -> ByteString+txKey c h = c.prefix <> "tx:" <> encode h++spKey :: Cache -> TxHash -> ByteString+spKey c h = c.prefix <> "sp:" <> encode h++addrKey :: Cache -> Address -> ByteString+addrKey c a = c.prefix <> "a:" <> encode a++lruKey :: Cache -> ByteString+lruKey c = c.prefix <> "lru"++-- | Count something in the metrics, if they are enabled.+count :: Cache -> (CacheMetrics -> StatCounter) -> Int -> IO ()+count c f n = mapM_ (\m -> incrementCounter (f m) n) c.metrics++log' :: Cache -> LogLevel -> Text -> IO ()+log' c l = logAt l c.logger++-- | Whether Redis answers a ping within two seconds. The health check of+-- the web server asks, so that a store whose Redis is gone says that it is+-- answering from its own database alone.+cacheAlive :: Cache -> IO Bool+cacheAlive c =+  handleAny (\_ -> return False) $+    timeout 2000000 (R.runRedis c.redis R.ping) <&> \case+      Just (Right _) -> True+      _ -> False++-- | Run Redis commands, throwing if Redis answers with an error.+run :: Cache -> Redis (Either Reply a) -> IO a+run c f =+  R.runRedis c.redis f >>= \case+    Right x -> return x+    Left e -> fail ("Redis error: " <> show e)++-------------------------------------------------------------------------------+-- Scores+-------------------------------------------------------------------------------++-- | Score of a position in a Redis sorted set, such that sorting by score+-- ascending sorts from newest to oldest. Mempool transactions get negative+-- scores from their time, so they come first; confirmed ones get positive+-- scores that decrease with height and position. Height and position are+-- packed into the 53 bits that a double holds exactly.+--+-- Between equal scores Redis orders members by their bytes, which for+-- transaction hashes and unspent outputs is the order that the database uses+-- ('Haskoin.Store.Common.txRefKey', 'Haskoin.Store.Common.unspentKey'), so a+-- cached page is the page the database gives.+blockRefScore :: BlockRef -> Double+blockRefScore BlockRef {height = h, position = p} =+  fromIntegral (0x001fffffffffffff - (h' .|. p'))+  where+    h' = (fromIntegral h .&. 0x07ffffff) `shift` 26 :: Word64+    p' = (fromIntegral p .&. 0x03ffffff) :: Word64+blockRefScore MemRef {timestamp = t} = negate t'+  where+    t' = fromIntegral (t .&. 0x001fffffffffffff)++-- | Inverse of 'blockRefScore'.+scoreBlockRef :: Double -> BlockRef+scoreBlockRef s+  | s < 0 = MemRef {timestamp = n}+  | otherwise = BlockRef {height = h, position = p}+  where+    n = truncate (abs s) :: Word64+    m = 0x001fffffffffffff - n+    h = fromIntegral (m `shift` (-26))+    p = fromIntegral (m .&. 0x03ffffff)++-------------------------------------------------------------------------------+-- Reading+-------------------------------------------------------------------------------++-- | Balances of a cached xpub, or 'Nothing' if it is not cached. Marks the+-- xpub as used. An entry that cannot be decoded is deleted and counts as not+-- cached.+cacheXPubBals :: Cache -> XPubSpec -> IO (Maybe [XPubBal])+cacheXPubBals c x = do+  touch <- touchArgs c x+  r <- run c $ do+    b <- R.get (xkey c x "bal")+    -- marking an xpub that is not cached is harmless: eviction removes it+    forM_ touch $ \now -> R.zadd (lruKey c) [(fromIntegral now, xpubId x)]+    return b+  case r of+    Nothing -> count c (.misses) 1 >> return Nothing+    Just bs -> case decode bs of+      Right bals -> count c (.hits) 1 >> return (Just bals)+      Left _ -> do+        count c (.misses) 1+        log' c LevelWarn "Cached xpub balances cannot be decoded; deleting them"+        void (cacheDelete c x)+        return Nothing++-- | Whether an xpub is cached.+cacheIsCached :: Cache -> XPubSpec -> IO Bool+cacheIsCached c x = run c (R.exists (xkey c x "meta"))++-- | Number of transactions of a cached xpub.+cacheXPubTxCount :: Cache -> XPubSpec -> IO (Maybe Word32)+cacheXPubTxCount c x = do+  touch <- touchArgs c x+  -- sent together, in one round trip+  (cached, n) <- run c $ do+    e <- R.exists (xkey c x "meta")+    z <- R.zcard (xkey c x "txs")+    forM_ touch $ \now -> R.zadd (lruKey c) [(fromIntegral now, xpubId x)]+    return ((,) <$> e <*> z)+  return (if cached then Just (fromIntegral n) else Nothing)++-- | Page of the transactions of a cached xpub, newest first, as the database+-- would give it.+cacheXPubTxs :: Cache -> XPubSpec -> Limits -> IO (Maybe [TxRef])+cacheXPubTxs c x limits = do+  xs <- page c x "txs" limits (\th -> (0, encode th))+  return $ xs <&> \ys -> [TxRef {txid = t, block = scoreBlockRef sc} | (bs, sc) <- ys, Right t <- [decode bs]]++-- | Page of the unspent outputs of a cached xpub, newest first, given its+-- balances, which give the paths of the addresses.+cacheXPubUnspents :: Cache -> XPubSpec -> [XPubBal] -> Limits -> IO (Maybe [XPubUnspent])+cacheXPubUnspents c x bals limits = do+  -- members are serialized unspent outputs: their block position, which is+  -- nine bytes long, then their output point+  xs <- page c x "utxo" limits (\th -> (9, encode (OutPoint th 0)))+  let paths = HashMap.fromList [(b.balance.address, b.path) | b <- bals]+  return $+    xs <&> \ys ->+      [ XPubUnspent {unspent = u, path = p}+      | (bs, _) <- ys,+        Right u <- [decode bs],+        Just a <- [u.address],+        Just p <- [HashMap.lookup a paths]+      ]++-- | How often a store marks an xpub as used, in milliseconds. Marking it on+-- every read would make every read a write; eviction only needs to know+-- which xpubs were used recently.+touchEvery :: Integer+touchEvery = 10000++-- | The time to mark an xpub as used with, if this store has not marked it+-- in the last 'touchEvery' milliseconds.+touchArgs :: Cache -> XPubSpec -> IO (Maybe Integer)+touchArgs c x = do+  now <- nowMs+  atomicModifyIORef' c.touched $ \m ->+    case HashMap.lookup (xpubId x) m of+      Just t | now - t < touchEvery -> (m, Nothing)+      _ ->+        -- forget everything now and then, so that the map stays small+        let m' = if HashMap.size m > 100000 then HashMap.empty else m+         in (HashMap.insert (xpubId x) now m', Just now)++-- | A page of a sorted set of a cached xpub, with the scores of its members,+-- or 'Nothing' if the xpub is not cached. One round trip to Redis: the+-- script finds where the page starts, as the database would, and marks the+-- xpub as used if it is time to.+--+-- A page that starts at a transaction starts at its position, which is the+-- score of the transaction in the sorted set of transactions of the xpub.+-- Members of the same score whose bytes, after a number of bytes, sort+-- before the bytes that the function gives for the transaction are skipped,+-- as the database skips them. A start transaction that is not in the xpub is+-- looked up in Redis or in the database, in a second round trip; a page+-- with a start that is found nowhere is empty.+page ::+  Cache ->+  XPubSpec ->+  ByteString ->+  Limits ->+  (TxHash -> (Int, ByteString)) ->+  IO (Maybe [(ByteString, Double)])+page c x set limits tieBytes = do+  touch <- touchArgs c x+  let common =+        [ xkey c x "meta",+          xkey c x set,+          xkey c x "txs",+          lruKey c,+          maybe "" num touch,+          xpubId x,+          num limits.offset,+          num limits.limit+        ]+      call extra = run c (R.eval pageScript [] (common <> extra))+  r <- case limits.start of+    Nothing -> call ["top"]+    Just (AtBlock h) -> call ["score", num' (blockRefScore (BlockRef h maxBound))]+    Just (AtTx th) -> do+      let (skip, bytes) = tieBytes th+      call ["tx", encode th, num skip, bytes] >>= \case+        ["nostart"] ->+          startOf th >>= \case+            Nothing -> return ["ok"]+            Just ref -> call ["at", num' (blockRefScore ref), num skip, bytes]+        y -> return y+  case r of+    "ok" : rest -> return (Just (pairs rest))+    _ -> return Nothing+  where+    pairs (m : sc : rest) | Just d <- readMaybe (C8.unpack sc) = (m, d) : pairs rest+    pairs _ = []+    startOf th =+      cacheTxData c [th] >>= \case+        [Just td] -> return (Just td.block)+        _ -> fmap (.block) <$> viewed c (\v -> L.getTxData v th)++-- | Bodies of transactions that Redis has, in the order of the hashes, with+-- the spenders of their outputs filled in. 'Nothing' for those it does not+-- have.+cacheTxData :: Cache -> [TxHash] -> IO [Maybe TxData]+cacheTxData _ [] = return []+cacheTxData c hs = do+  bodies <- run c (R.mget (NonEmpty.fromList (map (txKey c) hs)))+  spenders <- run c $ do+    rs <- forM (zip hs bodies) $ \(h, b) ->+      if isJust b then fmap Just <$> R.hgetall (spKey c h) else return (pure Nothing)+    return (sequence rs)+  return (zipWith fill bodies spenders)+  where+    fill (Just b) sp+      | Right td <- decode b =+          let ss =+                IntMap.fromList+                  [ (fromIntegral i, s)+                  | (k, v) <- fromMaybe [] sp,+                    Right (i :: Word32) <- [decode k],+                    Right s <- [decode v]+                  ]+           in Just (td :: TxData) {spenders = ss}+    fill _ _ = Nothing++-------------------------------------------------------------------------------+-- Changing+-------------------------------------------------------------------------------++-- | Cache an xpub that was just read from the database, if it has enough+-- used addresses, or enough transactions: a wallet that reuses a few+-- addresses for many payments is as slow to read from the database as one+-- with many addresses. Transactions are counted per address, so one that+-- touches two addresses counts twice, which is close enough to decide. The+-- xpub is loaded in the background; the caller keeps the answer it has.+cacheRequest :: Cache -> XPubSpec -> [XPubBal] -> IO ()+cacheRequest c x bals =+  when (length used >= c.minAddrs || sum (map (.balance.txs) used) >= fromIntegral c.minTxs) $+    push c (JobCache x)+  where+    used = filter (not . nullBalance . (.balance)) bals++-- | Give the worker something to do.+push :: Cache -> Job -> IO ()+push c j = atomically (modifyTVar' c.jobs (j :))++-- | Give the worker something to do in two seconds.+later :: Cache -> Job -> IO ()+later c j = void . forkIO $ threadDelay 2000000 >> push c j++-- | Remove an xpub from the cache. Returns whether it was cached.+cacheDelete :: Cache -> XPubSpec -> IO Bool+cacheDelete c x = do+  r <- run c (R.eval deleteScript [] [c.prefix, xpubId x])+  return (r == (1 :: Integer))++-- | Evict the least recently used xpubs until at most the given number are+-- cached. Returns how many were evicted. The cache does this by itself after+-- caching an xpub, down to its configured maximum.+cacheEvict :: Cache -> Int -> IO Int+cacheEvict c n = do+  evicted <- fromInteger <$> run c (R.eval evictScript [] [c.prefix, num n])+  count c (.evictions) evicted+  return evicted++-- | Bring the entry of an xpub in Redis up to date with the database, unless+-- another store holds its lock or Redis knows more than this store (see+-- 'Reload'). The transactions given are the ones that changed it: their+-- bodies are written again even if Redis has them.+--+-- A store never writes data that knows less than what is in Redis, so stores+-- that see the chain and the mempool at slightly different times do not undo+-- each other's work. Every body that the entry refers to is in Redis before+-- the entry is: the changes and the missing bodies are written in one Lua+-- script, which writes nothing if a body is missing, so a store that is+-- behind never finds a cached transaction that it cannot serve. Only the+-- differences are written, and the script writes nothing if the entry+-- changed since it was read (its generation), so Redis is held for the size+-- of the change, not of the xpub. Bodies that Redis already has, such as+-- those of the transactions that a block confirms, are rewritten first, in+-- small scripts ('rewriteBodies'), so that the confirmation of a large block+-- does not hold Redis in one long script.+--+-- = Reading only what changed+--+-- Working out the differences can take one of two paths.+--+-- * __Incremental__ ('incrementalDiff'): only the transactions that changed+--   are read, with the outputs that they create and spend at addresses of+--   the xpub. The stress test (@stress\/results@) showed that reading the+--   whole xpub from both the database and Redis was almost all the time of a+--   reload, growing with the size of the xpub to seconds at 100,000+--   transactions; the changes of one block or transaction are a few reads,+--   whatever the size of the xpub.+--+-- * __Full__: the whole xpub is read from the database and from Redis, and+--   compared. This is what a first caching does, and the fallback whenever+--   the incremental path cannot be sure of its answer.+--+-- The incremental path relies on every change of the xpub having been+-- applied to the entry, which is what the claims of events are for, but a+-- store can miss events: while it catches up, after a restart, or after an+-- error. So it is only taken when the entry shows no sign of having missed+-- one, and it checks its own result:+--+-- * the entry is in the current format, stamped with the best block of this+--   store or the one before it (a larger gap means blocks it may have+--   missed), and was fully reloaded less than 'fullReloadAfter' ago;+--+-- * the balances of every address of the xpub are read from the database,+--   which is cheap, a read per address; an address that no changed+--   transaction touches must have the balance that Redis has. Under load it+--   often does not, because the database is already ahead of the events+--   being applied, so the newer transactions of those addresses are added to+--   the changes first ('catchUp'), and only a balance that they do not+--   explain means a change was missed;+--+-- * no address with transactions enters or leaves the gap, as its history+--   would have to be read whole.+--+-- Otherwise the full path runs. A missed change that leaves every balance+-- as it was, such as the confirmation of a transaction that only spends from+-- the xpub, is the one thing the checks cannot see, and the regular full+-- reload bounds how long it can last.+cacheRefresh :: Cache -> XPubSpec -> HashSet TxHash -> IO Reload+cacheRefresh c x changed =+  fromMaybe Locked <$> withLock c x (timed (go (3 :: Int) []))+  where+    go tries extra = do+      m <- readMeta c x+      height <- bestHeight c+      now <- nowMs+      if not (mayReplace m.stamp height False)+        then behind+        else do+          bals <- viewed c (\v -> L.xPubBals v x)+          fast <-+            if incremental m height now+              then incrementalDiff c x changed m bals+              else return Fallback+          diff <- case fast of+            Apply d -> return (Just d)+            KnowsMore -> return Nothing+            Fallback -> fullDiff c x changed m height bals now+          case diff of+            Nothing -> behind+            Just d -> commit tries extra m height now bals d+    commit tries extra m height now bals d = do+      bodies <- catMaybes <$> viewed c (\v -> L.getManyTxData v (nub (extra <> d.wanted))) >>= rewriteBodies c+      let args =+            [c.prefix, xpubId x, num height, num now, encode x, encode bals, m.gen]+              <> [if d.full then num now else ""]+              <> counted d.addrAdd+              <> counted d.addrDel+              <> counted2 [(t, num' sc) | (t, sc) <- d.txAdd]+              <> counted d.txDel+              <> counted3 [(op, u, num' sc) | (op, u, sc) <- d.utxoAdd]+              <> counted2 d.utxoDel+              <> bodyArgs bodies+      run c (R.eval commitScript [] args) >>= \case+        ["ok"] -> do+          void (cacheEvict c c.maxXPubs)+          count c (.reloads) 1+          when d.full $ count c (.fullReloads) 1+          atomicModifyIORef' c.counters $ \k ->+            ( k {reloads = k.reloads + 1, fullReloads = k.fullReloads + fromEnum d.full} :: CacheCounters,+              ()+            )+          return Reloaded+        ("missing" : missing)+          | tries > 0 -> go (tries - 1) (rights (map decode missing))+        ["changed"]+          | tries > 0 -> go (tries - 1) extra+        _ -> behind+    -- whether the entry may be brought up to date incrementally+    incremental m height now =+      m.ver == "3"+        && not (HashSet.null changed)+        && maybe False (\s -> toInteger height - s <= 1) m.stamp+        && maybe False (\f -> now - f < fullReloadAfter x) m.full+    behind = do+      count c (.stale) 1+      atomicModifyIORef' c.counters (\k -> (k {behind = k.behind + 1} :: CacheCounters, ()))+      return Behind+    timed f = do+      start <- nowMs+      r <- f+      end <- nowMs+      forM_ c.metrics $ \m -> addTiming m.reloadTime (fromInteger (end - start))+      return r++-- | Longest time, in milliseconds, between full reloads of an entry: ten+-- minutes; each xpub has its own interval up to this ('fullReloadAfter'). It+-- bounds how long a change that the checks of the incremental path cannot+-- see stays out of the cache. A full reload of the largest xpubs of the+-- stress test takes a few seconds and holds the xpub in memory while it+-- runs, so it is kept rare.+fullReloadEvery :: Integer+fullReloadEvery = 10 * 60 * 1000++-- | Time, in milliseconds, after its last full reload when an entry is read+-- whole again: between half of 'fullReloadEvery' and all of it, the same for+-- an xpub on every store, and different from one xpub to the next.+--+-- With one interval for every entry, entries cached together are reloaded+-- whole together, every time. In the full stress test the four xpubs did so+-- every ten minutes, and the heap of the server went from under 100 MiB to+-- 785 MiB for a few seconds, holding all four at once. Each xpub gets its own+-- interval instead, so such entries drift apart from their first scheduled+-- full reload. It comes from the hash of the xpub, not from a random number,+-- so that it needs no state and every store sharing Redis agrees on it.+fullReloadAfter :: XPubSpec -> Integer+fullReloadAfter x = half + spread `mod` half+  where+    half = fullReloadEvery `div` 2+    spread = BS.foldl' (\a w -> a * 256 + toInteger w) 0 (BS.take 4 (encode (sha256 (encode x))))++-- | What to write to bring an entry up to date. Members of the sets are+-- serialized: addresses, transaction hashes, and unspent outputs, which go+-- with their outpoints.+data Diff = Diff+  { addrAdd :: ![ByteString],+    addrDel :: ![ByteString],+    -- | transactions to add or move, with their scores+    txAdd :: ![(ByteString, Double)],+    txDel :: ![ByteString],+    -- | unspent outputs to add, with their outpoints and scores+    utxoAdd :: ![(ByteString, ByteString, Double)],+    -- | outpoints of unspent outputs to remove, with the member if known; the+    -- script finds it by its outpoint otherwise+    utxoDel :: ![(ByteString, ByteString)],+    -- | transactions whose bodies to write+    wanted :: ![TxHash],+    -- | whether the whole xpub was read, which the entry records+    full :: !Bool+  }++-- | The outcome of trying the incremental path.+data Incremental+  = -- | these are the differences+    Apply !Diff+  | -- | Redis has a changed transaction that this store has never seen+    KnowsMore+  | -- | the incremental path cannot be sure; read the whole xpub+    Fallback++-- | The differences of an entry from the transactions that changed and the+-- balances of the xpub, without reading its history; see 'cacheRefresh'.+--+-- When the balances show that the database knows of changes beyond the ones+-- given, the newest transactions of the addresses whose balances moved are+-- added to them ('catchUp'), and the differences are worked out again; only+-- if that does not account for every balance does it fall back.+incrementalDiff :: Cache -> XPubSpec -> HashSet TxHash -> Meta -> [XPubBal] -> IO Incremental+incrementalDiff c x changed m bals =+  case (decode m.bal :: Either String [XPubBal], m.stamp) of+    (Right old, Just stamp) ->+      incrementalFrom c x old bals changed >>= \case+        Missed missed ->+          catchUp c x stamp missed >>= \case+            Just extra+              | not (extra `HashSet.isSubsetOf` changed) ->+                  incrementalFrom c x old bals (changed <> extra) <&> \case+                    Missed _ -> Fallback+                    Settled r -> r+            _ -> return Fallback+        Settled r -> return r+    _ -> return Fallback++-- | The outcome of working out the differences from a set of changes.+data Attempt+  = -- | the balances of these addresses moved, but no change touches them+    Missed ![Address]+  | Settled !Incremental++-- | The differences of an entry from its old balances, its new ones, and the+-- transactions that changed it.+incrementalFrom :: Cache -> XPubSpec -> [XPubBal] -> [XPubBal] -> HashSet TxHash -> IO Attempt+incrementalFrom c x old bals changed = do+  let hashes = HashSet.toList changed+  stored <- viewed c (\v -> L.getManyTxData v hashes)+  -- those that the database does not have because this store deleted+  -- them count as known, and as deleted+  gone <- readIORef c.lost+  dropped <- lostTxs c [h | (h, Nothing) <- zip hashes stored, HashSet.member h gone]+  let found = HashMap.fromList [(txHash td.tx, td) | td <- dropped]+      tds = [maybe (HashMap.lookup h found) Just td | (h, td) <- zip hashes stored]+  let newMap = HashMap.fromList [(b.balance.address, b.balance) | b <- bals]+      oldMap = HashMap.fromList [(b.balance.address, b.balance) | b <- old]+      entering = [b | (a, b) <- HashMap.toList newMap, not (HashMap.member a oldMap)]+      leaving = [b | (a, b) <- HashMap.toList oldMap, not (HashMap.member a newMap)]+      known = catMaybes tds+      unknown = [h | (h, Nothing) <- zip hashes tds]+      ours a = HashMap.member a newMap+      touched = HashSet.fromList [a | td <- known, a <- txAddresses c td, ours a]+      missed =+        [ a+        | (a, b) <- HashMap.toList newMap,+          Just o <- [HashMap.lookup a oldMap],+          o /= b,+          not (HashSet.member a touched)+        ]+  if+    | any (\b -> b.txs > 0) (entering <> leaving) -> return (Settled Fallback)+    | not (null missed) -> return (Missed missed)+    | otherwise -> do+        inRedis <- run c (sequence <$> mapM (R.zscore (xkey c x "txs") . encode) unknown)+        if any isJust inRedis+          then return (Settled KnowsMore)+          else do+            let active td = not td.deleted && any ours (txAddresses c td)+                ops = nub (concatMap (outpointsAt c ours) known)+            us <- viewed c (\v -> mapM (L.getUnspent v) ops)+            return . Settled . Apply $+              Diff+                { addrAdd = [encode b.address | b <- entering],+                  addrDel = [encode b.address | b <- leaving],+                  txAdd = [(encode (txHash td.tx), blockRefScore td.block) | td <- known, active td],+                  txDel = [encode (txHash td.tx) | td <- known, not (active td)],+                  utxoAdd = [(encode op, encode u, blockRefScore u.block) | (op, Just u) <- zip ops us],+                  utxoDel = [(encode op, "") | (op, Nothing) <- zip ops us],+                  wanted = [txHash td.tx | td <- known, active td],+                  full = False+                }++-- | Changes of an entry that the database has and that it was not told of:+-- what a store finds when its database runs ahead of the events it is+-- applying, as it does under load, when the balances it reads after a batch+-- already include the transactions of the next. 'Nothing' if there are more+-- than 'catchUpMost', when reading the whole xpub is as cheap.+--+-- The entry is stamped with a block that this store had whole, so what can+-- have changed since is:+--+-- * transactions in the mempool or in a later block, which are first in the+--   index of an address, newest first, and are read from it for each address+--   whose balance moved, down to the stamp;+--+-- * transactions in the mempool when the entry was written that have been+--   confirmed or dropped since, found from the mempool transactions of the+--   entry, which are few.+--+-- A reorganisation that replaces the block of the stamp is not covered, and+-- the balances catch it: if the transactions found do not account for every+-- balance, the full path runs.+catchUp :: Cache -> XPubSpec -> Integer -> [Address] -> IO (Maybe (HashSet TxHash))+catchUp c x stamp addrs = do+  newer <- sequence <$> mapM addressNewer addrs+  pool <- run c (R.zrangebyscoreLimit (xkey c x "txs") (-1 / 0) (-0.5) 0 (toInteger catchUpMost + 1))+  let poolHashes = rights (map decode pool)+  tds <- viewed c (\v -> L.getManyTxData v poolHashes)+  let settled = [h | (h, td) <- zip poolHashes tds, maybe True left td]+      found = HashSet.fromList (settled <> concat (fromMaybe [] newer))+  return $+    if isNothing newer || length pool > catchUpMost || HashSet.size found > catchUpMost+      then Nothing+      else Just found+  where+    -- a mempool transaction of the entry that is no longer in the mempool+    left td = td.deleted || not (isMem td.block)+    isMem MemRef {} = True+    isMem BlockRef {} = False+    after t = case t.block of+      MemRef {} -> True+      BlockRef {height = h} -> toInteger h > stamp+    chunk = 100 :: Word32+    addressNewer a = loop 0 []+      where+        loop off acc = do+          ts <- viewed c (\v -> L.getAddressTxs v a Limits {limit = chunk, offset = off, start = Nothing})+          let (new, rest) = span after ts+              acc' = acc <> map (.txid) new+          if+            | length acc' > catchUpMost -> return Nothing+            | not (null rest) || length ts < fromIntegral chunk -> return (Just acc')+            | otherwise -> loop (off + chunk) acc'++-- | Most transactions that 'catchUp' adds to the changes of a reload. Each+-- costs a few reads; a full reload of the xpubs of the stress test costs+-- tens of thousands.+catchUpMost :: Int+catchUpMost = 1000++-- | The differences of an entry from the whole xpub, read from the database+-- and from Redis. 'Nothing' if Redis has a transaction of the xpub that this+-- store has never seen.+fullDiff :: Cache -> XPubSpec -> HashSet TxHash -> Meta -> Word32 -> [XPubBal] -> Integer -> IO (Maybe Diff)+fullDiff c x changed m height bals _ = do+  st <- readStored c x+  txs <- viewed c (\v -> L.xPubTxs v bals def')+  let newTxs = [(encode t.txid, blockRefScore t.block) | t <- txs]+      newTxSet = HashSet.fromList (map fst newTxs)+      txDel = [t | t <- HashMap.keys st.txs, not (HashSet.member t newTxSet)]+  -- a transaction that Redis has and this store has never seen. The store+  -- keeps nothing of a transaction that it deleted, so one that it does+  -- not have is taken for deleted, and not for unseen, if the cache heard+  -- of its deletion, or if the entry has it as unconfirmed for more than+  -- ten minutes: a store that is in step with the others has seen a+  -- transaction of the mempool by then, and one that is not does not get+  -- here.+  let asked = [(h, sc) | t <- txDel, Right h <- [decode t], Just sc <- [HashMap.lookup t st.txs]]+  known <- viewed c (\v -> L.getManyTxData v (map fst asked))+  gone <- readIORef c.lost+  now <- (`div` 1000) <$> nowMs+  let stale sc = sc < 0 && fromIntegral now - negate sc > 600+      unseen = [h | ((h, sc), Nothing) <- zip asked known, not (HashSet.member h gone), not (stale sc)]+  if not (mayReplace m.stamp height (not (null unseen)))+    then return Nothing+    else do+      utxos <- viewed c (\v -> L.xPubUnspents v bals def')+      let txAdd = [(t, sc) | (t, sc) <- newTxs, HashMap.lookup t st.txs /= Just sc]+          newUs = [(encode u.unspent.outpoint, encode u.unspent, blockRefScore u.unspent.block) | u <- utxos]+          newUSet = HashSet.fromList [u | (_, u, _) <- newUs]+          -- an entry of an older format has no index of outpoints yet, so+          -- every output is written, to build it+          utxoAdd+            | m.ver == "3" = [n | n@(_, u, _) <- newUs, not (HashSet.member u st.utxos)]+            | otherwise = newUs+          utxoDel =+            [ (either (const "") (encode . (.outpoint)) (decode u :: Either String Unspent), u)+            | u <- HashSet.toList st.utxos,+              not (HashSet.member u newUSet)+            ]+          newAs = HashSet.fromList [encode b.balance.address | b <- bals]+      return . Just $+        Diff+          { addrAdd = [a | a <- HashSet.toList newAs, not (HashSet.member a st.addrs)],+            addrDel = [a | a <- HashSet.toList st.addrs, not (HashSet.member a newAs)],+            txAdd,+            txDel,+            utxoAdd,+            utxoDel,+            -- bodies of the transactions that are new or moved, and of those+            -- that changed; the others are in Redis, kept there by this entry+            wanted =+              nub $+                [h | (t, _) <- txAdd, Right h <- [decode t]]+                  <> [t.txid | t <- txs, t.txid `HashSet.member` changed],+            full = True+          }+  where+    def' = Limits {limit = 0, offset = 0, start = Nothing}++-- | Addresses that a transaction spends from or pays to.+txAddresses :: Cache -> TxData -> [Address]+txAddresses c td =+  nub . rights $+    map (scriptToAddressBS c.db.net c.db.ctx . (.script)) (IntMap.elems td.prevs)+      <> map (scriptToAddressBS c.db.net c.db.ctx . (.script)) td.tx.outputs++-- | Outpoints whose unspent output a transaction may change at addresses that+-- pass a test: those it creates, and those it spends.+outpointsAt :: Cache -> (Address -> Bool) -> TxData -> [OutPoint]+outpointsAt c ours td =+  [OutPoint (txHash td.tx) i | (i, o) <- zip [0 ..] td.tx.outputs, at o.script]+    <> [ inp.outpoint+       | (i, inp) <- zip [0 :: Int ..] td.tx.inputs,+         Just prev <- [IntMap.lookup i td.prevs],+         at prev.script+       ]+  where+    at s = either (const False) ours (scriptToAddressBS c.db.net c.db.ctx s)++-- | What a reload needs to know of an entry first: the height it is stamped+-- with, its generation, which every commit increments, the version of its+-- format, the time of its last full reload, and its balances, serialized.+data Meta = Meta+  { stamp :: !(Maybe Integer),+    gen :: !ByteString,+    ver :: !ByteString,+    full :: !(Maybe Integer),+    bal :: !ByteString+  }++-- | Read what a reload needs to know of an entry, in one round trip.+readMeta :: Cache -> XPubSpec -> IO Meta+readMeta c x = do+  (fs, b) <- run c $ do+    fs <- R.hmget (xkey c x "meta") ("height" NonEmpty.:| ["gen", "ver", "full"])+    b <- R.get (xkey c x "bal")+    return ((,) <$> fs <*> b)+  let field k = join (lookup k (zip [0 :: Int ..] fs))+      number = (readMaybe . C8.unpack =<<)+  return+    Meta+      { stamp = number (field 0),+        gen = fromMaybe "" (field 1),+        ver = fromMaybe "" (field 2),+        full = number (field 3),+        bal = fromMaybe "" b+      }++-- | The sets of an entry, as a full reload compares them with the database.+data Stored = Stored+  { txs :: !(HashMap ByteString Double),+    utxos :: !(HashSet ByteString),+    addrs :: !(HashSet ByteString)+  }++-- | Read the sets of an entry. The sorted sets are read in chunks, so that+-- no command holds Redis for long however large the xpub. The chunks are not+-- read atomically, but nothing changes the entry while this store holds its+-- lock except deleting or evicting it, which changes its generation, and the+-- commit checks the generation.+readStored :: Cache -> XPubSpec -> IO Stored+readStored c x = do+  as <- run c (R.smembers (xkey c x "addrs"))+  ts <- chunks (\i j -> R.zrangeWithscores (xkey c x "txs") i j)+  us <- chunks (\i j -> R.zrange (xkey c x "utxo") i j)+  return+    Stored+      { txs = HashMap.fromList ts,+        utxos = HashSet.fromList us,+        addrs = HashSet.fromList as+      }+  where+    size = 5000 :: Integer+    chunks :: (Integer -> Integer -> Redis (Either Reply [a])) -> IO [a]+    chunks f = loop 0+      where+        loop i = do+          xs <- run c (f i (i + size - 1))+          if toInteger (length xs) < size then return xs else (xs <>) <$> loop (i + size)++-- | Rewrite the bodies that Redis already has, in scripts of 'bodyChunk'+-- bodies each, and give back the others, for the commit to write.+--+-- A block confirms many transactions of a busy xpub at once, and each needs+-- its body written again, with its block and spenders. In one script, with+-- the scores and outputs of the entry, that held Redis for 10 to 30 ms at+-- the confirmation of each block in the stress test, while every other+-- client waited. Bodies are shared by every entry that has the transaction,+-- and are written whole from the database, so writing them before the entry+-- is safe: a body is never older than the entry that refers to it, which is+-- what matters (see 'cacheRefresh'), and other entries already see bodies+-- written by commits of other xpubs before their own. Only bodies that are+-- there are rewritten, so that a commit that then fails leaves nothing+-- behind; and if one goes between these scripts and the commit, the commit+-- reports it missing and is retried with it.+rewriteBodies :: Cache -> [TxData] -> IO [TxData]+rewriteBodies c tds =+  fmap concat . forM (chunks tds) $ \ch -> do+    absent <- HashSet.fromList <$> run c (R.eval bodiesScript [] (c.prefix : bodyArgs ch))+    return [td | td <- ch, encode (txHash td.tx) `HashSet.member` absent]+  where+    chunks [] = []+    chunks xs = let (a, b) = splitAt bodyChunk xs in a : chunks b++-- | Bodies rewritten by one script of 'rewriteBodies': each is a few+-- commands, so a chunk holds Redis for a millisecond or two.+bodyChunk :: Int+bodyChunk = 200++-- | Arguments for bodies and their spenders: the number of bodies, the hash+-- and body of each, the number of spenders, and the transaction, output index+-- and spender of each.+bodyArgs :: [TxData] -> [ByteString]+bodyArgs tds =+  counted2 [(encode (txHash td.tx), encode (td :: TxData) {spenders = IntMap.empty}) | td <- tds]+    <> counted3+      [ (encode (txHash td.tx), encode (fromIntegral i :: Word32), encode s)+      | td <- tds,+        (i, s) <- IntMap.toList td.spenders+      ]++counted :: [ByteString] -> [ByteString]+counted xs = num (length xs) : xs++counted2 :: [(ByteString, ByteString)] -> [ByteString]+counted2 xs = num (length xs) : concat [[a, b] | (a, b) <- xs]++counted3 :: [(ByteString, ByteString, ByteString)] -> [ByteString]+counted3 xs = num (length xs) : concat [[a, b, d] | (a, b, d) <- xs]++num :: (Integral a) => a -> ByteString+num = C8.pack . show . toInteger++num' :: Double -> ByteString+num' = C8.pack . show++nowMs :: IO Integer+nowMs = round . (* 1000) <$> getPOSIXTime++-- | Height of the best block of the database, 0 if none.+bestHeight :: Cache -> IO Word32+bestHeight c =+  Q.getBestBlock c.db >>= \case+    Nothing -> return 0+    Just h -> maybe 0 (.height) <$> Q.getBlock c.db h++-- | Whether the database has the best header of the node.+inSync :: Cache -> IO Bool+inSync c =+  Q.getBestBlock c.db >>= \case+    Nothing -> return False+    Just bb -> do+      cb <- chainGetBest c.chain+      return (cb.height > 0 && headerHash cb.header == bb)++-- | Run an action holding the lock of an xpub. 'Nothing' if another store+-- holds it. The lock expires after two minutes in case its holder dies.+withLock :: Cache -> XPubSpec -> IO a -> IO (Maybe a)+withLock c x f = do+  token <- C8.pack . show <$> (randomIO :: IO Word64)+  let key = c.prefix <> "lock:" <> xpubId x+      opts =+        R.SetOpts+          { R.setSeconds = Nothing,+            R.setMilliseconds = Just 120000,+            R.setUnixSeconds = Nothing,+            R.setUnixMilliseconds = Nothing,+            R.setCondition = Just R.Nx,+            R.setKeepTTL = False+          }+  R.runRedis c.redis (R.setOpts key token opts) >>= \case+    Right R.Ok -> do+      r <- f+      void (run c (R.eval unlockScript [key] [token] :: Redis (Either Reply Integer)))+      return (Just r)+    _ -> return Nothing++-------------------------------------------------------------------------------+-- Keeping up with the store+-------------------------------------------------------------------------------++-- | Do the work of the cache, forever.+--+-- A store that is not in sync ignores events and requests: the stores that+-- are ahead have already done or will do the work, and this one would only+-- find that Redis knows more. Once in sync, each event is claimed per cached+-- xpub that it changes ('claim'): the first store to claim a change reloads+-- the xpub and marks the claim done, and the others skip it, so a change is+-- applied once however many stores share the Redis, and stores that catch up+-- find their events done. A change that a store could not apply because+-- Redis knew more is released for another store to take; one that another+-- store is busy with is looked at again later.+worker :: Cache -> IO ()+worker c = do+  lastBest <- newIORef Nothing+  loopForever $ do+    js <- atomically $ do+      xs <- readTVar c.jobs+      checkSTM (not (null xs))+      writeTVar c.jobs []+      return (reverse xs)+    synced <- inSync c+    if synced+      then handleAny (\e -> log' c LevelError ("Cache: " <> cs (show e))) (work c lastBest js)+      else releaseClaims c (concat [ks | JobHeld _ _ ks <- js])++-- | Do a batch of jobs.+work :: Cache -> IORef (Maybe BlockHash) -> [Job] -> IO ()+work c lastBest js = do+  fromEvents <- concat <$> mapM (eventChanges c lastBest) [e | JobEvent e <- js]+  let toClaim = fromEvents <> [(k, x, ts) | JobClaim x ts ks <- js, k <- ks]+  claimed <- fmap catMaybes . forM toClaim $ \(k, x, ts) ->+    afterClaim <$> claim c k >>= \case+      ReloadNow -> return (Just (x, (ts, [k])))+      Skip -> return Nothing+      ClaimLater -> later c (JobClaim x ts [k]) >> return Nothing+  let held =+        HashMap.fromListWith+          (\(a, b) (a', b') -> (a <> a', b <> b'))+          (claimed <> [(x, (ts, ks)) | JobHeld x ts ks <- js])+  pooledForConcurrentlyN_ parallelReloads (HashMap.toList held) $ \(x, (ts, ks)) ->+    afterReload <$> reload x ts >>= \case+      Finish -> finishClaims c ks+      Release -> releaseClaims c ks+      Hold -> extendClaims c ks >> later c (JobHeld x ts ks)+  pooledForConcurrentlyN_ parallelReloads (nub [x | JobCache x <- js, not (HashMap.member x held)]) $ \x ->+    cacheIsCached c x >>= \case+      True -> return ()+      False ->+        reload x HashSet.empty >>= \case+          Locked -> later c (JobCache x)+          _ -> return ()+  where+    reload x ts =+      tryAny (cacheRefresh c x ts) >>= \case+        Right r -> return r+        Left e -> do+          log' c LevelError ("Could not cache xpub: " <> cs (show e))+          return Locked++-- | How many xpubs a store reloads at once. Each holds the lock of its xpub,+-- so two stores never reload the same one; reloading several at once keeps+-- one large xpub from holding up the others.+parallelReloads :: Int+parallelReloads = 4++-- | Changes that an event makes to cached xpubs: for each xpub, the key of+-- the claim for the change, and the transactions that make it. Also keeps the+-- spenders of cached transactions up to date, which is harmless to repeat.+eventChanges :: Cache -> IORef (Maybe BlockHash) -> StoreEvent -> IO [(ByteString, XPubSpec, HashSet TxHash)]+eventChanges c lastBest e = do+  (name, tds) <- case e of+    StoreBestBlock h -> do+      prev <- readIORef lastBest+      writeIORef lastBest (Just h)+      ("b:" <> encode h,) <$> blockTxs c prev h+    StoreMempoolNew t -> ("t+:" <> encode t,) . maybeToList <$> viewed c (\v -> L.getTxData v t)+    -- the store keeps nothing of a transaction that it deleted. Redis has+    -- the body of every transaction of a cached xpub, so if it does not+    -- have this one, no entry does either+    StoreMempoolDelete t -> do+      noteLost c [t]+      ("t-:" <> encode t,) <$> lostTxs c [t]+    _ -> return ("", [])+  if null tds+    then return []+    else do+      updateSpenders c tds+      affected <- affectedXPubs c tds+      return [(c.prefix <> "ev:" <> name <> ":" <> xpubId x, x, ts) | (x, ts) <- affected]++-- | Claim a change. A claim is held for two minutes, so that a store that+-- dies while holding it does not keep others from the change for long.+claim :: Cache -> ByteString -> IO Claim+claim c k = do+  let opts =+        R.SetOpts+          { R.setSeconds = Nothing,+            R.setMilliseconds = Just 120000,+            R.setUnixSeconds = Nothing,+            R.setUnixMilliseconds = Nothing,+            R.setCondition = Just R.Nx,+            R.setKeepTTL = False+          }+  R.runRedis c.redis (R.setOpts k "busy" opts) >>= \case+    Right R.Ok -> return Claimed+    _ ->+      run c (R.get k) <&> \case+        Just "done" -> Done+        _ -> Busy++-- | Mark changes as applied, for an hour: long enough for every store in+-- sync to see the same events.+finishClaims :: Cache -> [ByteString] -> IO ()+finishClaims _ [] = return ()+finishClaims c ks =+  void . run c $ sequence <$> mapM (\k -> R.setex k 3600 "done") ks++-- | Give up changes, for another store to apply.+releaseClaims :: Cache -> [ByteString] -> IO ()+releaseClaims _ [] = return ()+releaseClaims c ks = void (run c (R.del (NonEmpty.fromList ks)))++-- | Hold changes for another two minutes.+extendClaims :: Cache -> [ByteString] -> IO ()+extendClaims _ [] = return ()+extendClaims c ks = void . run c $ sequence <$> mapM (`R.pexpire` 120000) ks++-- | Pass the events of the store that may change cached xpubs to the worker.+listener :: Cache -> Inbox StoreEvent -> IO ()+listener c events =+  loopForever $+    receive events >>= \case+      e@StoreBestBlock {} -> push c (JobEvent e)+      e@StoreMempoolNew {} -> push c (JobEvent e)+      e@StoreMempoolDelete {} -> push c (JobEvent e)+      _ -> return ()++-- | Transactions of a new best block, and of the blocks that a+-- reorganisation took out of the chain since the previous best block.+blockTxs :: Cache -> Maybe BlockHash -> BlockHash -> IO [TxData]+blockTxs c prev h =+  Q.getBlock c.db h >>= \case+    Nothing -> return []+    Just bd -> do+      reverted <- case prev of+        Just p | p /= bd.header.prev && p /= h -> walk p+        _ -> return []+      let hashes = bd.txs <> reverted+      found <- viewed c (\v -> L.getManyTxData v hashes)+      -- a transaction of a reverted block that did not go back to the+      -- mempool, as one that spends its coinbase does not, is gone from+      -- the database. Redis has its body if a cached xpub has it.+      let missing = [h' | (h', Nothing) <- zip hashes found]+      noteLost c missing+      (catMaybes found <>) <$> lostTxs c missing+  where+    -- blocks that are no longer in the main chain, from the old best block+    walk p =+      Q.getBlock c.db p >>= \case+        Just b | not b.main -> (b.txs <>) <$> walk b.header.prev+        _ -> return []++-- | Note that the store deleted transactions.+noteLost :: Cache -> [TxHash] -> IO ()+noteLost _ [] = return ()+noteLost c hs = atomicModifyIORef' c.lost $ \seen ->+  let more = foldr HashSet.insert seen hs+   in (if HashSet.size more > 100000 then HashSet.fromList hs else more, ())++-- | The bodies that Redis has of transactions that the store deleted,+-- marked as deleted. Redis has the body of every transaction of a cached+-- xpub, so one that it does not have is in no entry.+lostTxs :: Cache -> [TxHash] -> IO [TxData]+lostTxs c hs = map (\td -> (td :: TxData) {deleted = True}) . catMaybes <$> cacheTxData c hs++-- | Cached xpubs with an address that these transactions spend from or pay+-- to, with the transactions that touch each.+affectedXPubs :: Cache -> [TxData] -> IO [(XPubSpec, HashSet TxHash)]+affectedXPubs c tds = do+  let net = c.db.net+      ctx = c.db.ctx+      addrsOf :: TxData -> [Address]+      addrsOf td =+        nub . rights $+          map (scriptToAddressBS net ctx . (.script)) (IntMap.elems td.prevs)+            <> map (scriptToAddressBS net ctx . (.script)) td.tx.outputs+      byAddr =+        HashMap.fromListWith (<>) [(a, HashSet.singleton (txHash td.tx)) | td <- tds, a <- addrsOf td]+      addrs = HashMap.keys byAddr+  ids <- run c (sequence <$> mapM (R.smembers . addrKey c) addrs)+  let byId =+        HashMap.fromListWith+          (<>)+          [(i, ts) | (a, is) <- zip addrs ids, i <- is, Just ts <- [HashMap.lookup a byAddr]]+      spec i = R.hget (c.prefix <> "x:" <> i <> ":meta") "spec"+  specs <- run c (sequence <$> mapM spec (HashMap.keys byId))+  return+    [ (x, ts)+    | (Just bs, ts) <- zip specs (HashMap.elems byId),+      Right x <- [decode bs]+    ]++-- | Record who spends the outputs of cached transactions: these+-- transactions spend them, or no longer do if they were deleted.+updateSpenders :: Cache -> [TxData] -> IO ()+updateSpenders c tds =+  unless (null quads) $+    void (run c (R.eval spendsScript [] args :: Redis (Either Reply Integer)))+  where+    args = c.prefix : num (length quads) : concat quads+    quads =+      [ [ encode i.outpoint.hash,+          encode i.outpoint.index,+          if td.deleted then "" else encode (Spender (txHash td.tx) n),+          encode (txHash td.tx)+        ]+      | td <- tds,+        (n, i) <- zip [0 ..] td.tx.inputs+      ]++-------------------------------------------------------------------------------+-- Lua scripts+-------------------------------------------------------------------------------++-- The scripts that Redis runs are in files of their own under+-- @src\/Haskoin\/Store\/Cache\/lua@, where they can be read, linted and run+-- with @redis-cli --eval@ on their own, and are embedded here when the module+-- is compiled. Embedding them, rather than reading them when the server+-- starts, keeps the library free of data files that a program using it would+-- have to install and find. Their comments are sent with every call, so keep+-- them short; the full description of each is the Haddock of its binding.++-- | Delete an xpub. Arguments: prefix, id. Returns 1 if it was cached.+deleteScript :: ByteString+deleteScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/delete.lua" >>= embedFile)++-- | Evict the least recently used xpubs past a number. Arguments: prefix,+-- maximum. Returns how many were evicted.+evictScript :: ByteString+evictScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/evict.lua" >>= embedFile)++-- | A page of a sorted set of a cached xpub; see 'page'. Arguments: the+-- keys of the meta hash, of the sorted set and of the sorted set of+-- transactions, the key of the set of recent use, the time to mark the xpub+-- used with (empty for not now), the id of the xpub, the offset and the limit+-- (0 for no limit), and how the page starts: @top@; @score@ and a score;+-- @at@, a score, a number of bytes and the bytes of the start; or @tx@, a+-- transaction hash, a number of bytes and the bytes of the start. Returns+-- @{\"none\"}@ if the xpub is not cached, @{\"nostart\"}@ if the+-- transaction of @tx@ is not one of the xpub, or @{\"ok\"}@ followed by+-- each member and its score.+--+-- Bytes are compared one by one: Lua compares strings by the collation of+-- the locale of the server, which need not be the order of their bytes that+-- Redis and the database sort members by.+pageScript :: ByteString+pageScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/page.lua" >>= embedFile)++-- | Apply the differences of an entry of an xpub, unless the stored one is+-- stamped with a higher block, or is not the generation they were computed+-- from. Arguments: prefix, id, height, time of use, serialized xpub,+-- balances, generation (empty for none); then, each preceded by its number,+-- the addresses to add, the addresses to remove, the transactions to add or+-- move with their scores, the transactions to remove, the unspent outputs to+-- add with their scores, the unspent outputs to remove, the bodies to write+-- with their hashes, and the spenders with their transaction and output+-- index. Returns @{\"stale\"}@ if the stored entry has a higher block,+-- @{\"changed\"}@ if its generation is not the one given,+-- @{\"missing\", ...}@ with the hashes of added transactions that have no+-- body in Redis nor in the arguments, or @{\"ok\"}@. Nothing is written+-- unless it returns @{\"ok\"}@.+--+-- A transaction removed from the entry loses this xpub from its set of+-- xpubs, and its body goes when no cached xpub has it any more.+commitScript :: ByteString+commitScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/commit.lua" >>= embedFile)++-- | Rewrite bodies that Redis has, with their spenders; see 'rewriteBodies'.+-- Arguments: prefix, then as at the end of 'commitScript': the number of+-- bodies, the hash and body of each, the number of spenders, and the+-- transaction, output index and spender of each. Returns the hashes of the+-- bodies that Redis does not have, which it does not write.+bodiesScript :: ByteString+bodiesScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/bodies.lua" >>= embedFile)++-- | Record or forget spenders of outputs of cached transactions. Arguments:+-- prefix, number of entries, then for each: hash of the transaction that+-- created the output, output index, spender (empty to forget) and hash of+-- the spending transaction. Only transactions whose body is cached are+-- touched, and a spender is only forgotten if it is still the one given.+spendsScript :: ByteString+spendsScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/spends.lua" >>= embedFile)++-- | Release a lock if it still holds the given token. Keys: lock. Arguments:+-- token.+unlockScript :: ByteString+unlockScript = $(makeRelativeToProject "src/Haskoin/Store/Cache/lua/unlock.lua" >>= embedFile)
+ src/Haskoin/Store/Cache/Protocol.hs view
@@ -0,0 +1,122 @@+-- |+-- Module      : Haskoin.Store.Cache.Protocol+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The rules that stores sharing one Redis follow to apply each change of a+-- cached xpub once, as pure decisions. "Haskoin.Store.Cache" carries them+-- out against Redis.+--+-- Each change (a block, or a transaction entering or leaving the mempool,+-- for one xpub) has a claim in Redis that goes through these states:+--+-- @+--            claim            reload succeeds+--   absent ─────────► busy ─────────────────► done (for an hour)+--     ▲                 │+--     └─────────────────┘ Redis knew more: release+-- @+--+-- A store that finds the claim busy tries again later; one that finds it+-- done skips the change. While it holds the claim, it reloads the xpub from+-- its database, unless the entry in Redis knows more than the database+-- ('mayReplace'), in which case it releases the claim and writes nothing.+--+-- = Why these rules+--+-- * __A change is applied once.__ The first store to see a change claims it;+--   stores that see it later, because they are behind, find it done. A set of+--   stores catching up does no work for changes already applied.+-- * __Stores never undo each other.__ An entry is stamped with the best block+--   of the store that wrote it, and no store replaces an entry stamped with a+--   higher block, or one with a transaction it has never seen. Without this+--   rule, a store that is behind would replace a newer entry with an older+--   one, and the next store would put the newer one back.+-- * __An entry is complete.__ The entry is written with every transaction+--   body that it needs, in one script, so a store that is behind can serve+--   it without its database.+module Haskoin.Store.Cache.Protocol+  ( -- * Claims+    Claim (..),+    OnClaim (..),+    afterClaim,++    -- * Reloads+    Reload (..),+    OnReload (..),+    afterReload,+    mayReplace,+  )+where++import Haskoin (BlockHeight)++-- | The state of the claim of a change, as a store that tries to claim it+-- finds it.+data Claim+  = -- | it was absent: this store holds it now+    Claimed+  | -- | another store applied the change+    Done+  | -- | another store holds the claim+    Busy+  deriving stock (Eq, Show, Enum, Bounded)++-- | What to do after trying to claim a change.+data OnClaim+  = -- | reload the xpub, holding the claim+    ReloadNow+  | -- | nothing: the change was applied+    Skip+  | -- | try to claim it again later, in case its holder gives it up+    ClaimLater+  deriving stock (Eq, Show)++-- | What to do after trying to claim a change.+afterClaim :: Claim -> OnClaim+afterClaim = \case+  Claimed -> ReloadNow+  Done -> Skip+  Busy -> ClaimLater++-- | What became of a reload.+data Reload+  = -- | the entry was replaced+    Reloaded+  | -- | Redis knows more about the xpub than this store: its entry is stamped+    -- with a higher block, or has a transaction this store has never seen. It+    -- is left for a store that knows more.+    Behind+  | -- | another store holds the lock of the xpub+    Locked+  deriving stock (Eq, Show, Enum, Bounded)++-- | What to do with the claims of the changes that a reload was for.+data OnReload+  = -- | mark them done+    Finish+  | -- | give them up, for a store that knows more+    Release+  | -- | keep holding them, and reload again later+    Hold+  deriving stock (Eq, Show)++-- | What to do with the claims of the changes that a reload was for.+afterReload :: Reload -> OnReload+afterReload = \case+  Reloaded -> Finish+  Behind -> Release+  Locked -> Hold++-- | Whether a store may replace the entry of an xpub: given the block height+-- that the entry is stamped with, if there is an entry, the height of the+-- best block of the store, and whether the entry has a transaction that the+-- store has never seen. An entry of the same height may be replaced, as it+-- can differ only in the mempool. The commit script checks the height again,+-- atomically, as another store may write between the check and the commit.+mayReplace :: Maybe Integer -> BlockHeight -> Bool -> Bool+mayReplace stamp height unknownTxs =+  maybe True (<= toInteger height) stamp && not unknownTxs
+ src/Haskoin/Store/Cache/lua/bodies.lua view
@@ -0,0 +1,25 @@+-- Rewrite bodies that Redis has, ahead of a commit: bodiesScript in Haskoin.Store.Cache.+local p = ARGV[1]+local nb = tonumber(ARGV[2])+local bi = 3+local si = bi + 2 * nb + 1+local ns = tonumber(ARGV[si - 1])+local written = {}+local absent = {}+for j = 0, nb - 1 do+  local t = ARGV[bi + 2 * j]+  if redis.call('EXISTS', p .. 'tx:' .. t) == 1 then+    redis.call('SET', p .. 'tx:' .. t, ARGV[bi + 2 * j + 1])+    redis.call('DEL', p .. 'sp:' .. t)+    written[t] = true+  else+    table.insert(absent, t)+  end+end+for j = 0, ns - 1 do+  local k = si + 3 * j+  if written[ARGV[k]] then+    redis.call('HSET', p .. 'sp:' .. ARGV[k], ARGV[k + 1], ARGV[k + 2])+  end+end+return absent
+ src/Haskoin/Store/Cache/lua/commit.lua view
@@ -0,0 +1,92 @@+-- Apply the differences of an entry of an xpub: commitScript in Haskoin.Store.Cache.+local p = ARGV[1]+local id = ARGV[2]+local h = tonumber(ARGV[3])+local xk = p .. 'x:' .. id .. ':'+local meta = xk .. 'meta'+local oh = redis.call('HGET', meta, 'height')+if oh and h < tonumber(oh) then return {'stale'} end+if (redis.call('HGET', meta, 'gen') or '') ~= ARGV[7] then return {'changed'} end+local full = ARGV[8]+local i = 9+local function list(width)+  local n = tonumber(ARGV[i])+  local s = i + 1+  i = s + width * n+  return s, n+end+local aa, naa = list(1)+local ad, nad = list(1)+local ta, nta = list(2)+local td, ntd = list(1)+local ua, nua = list(3)+local ud, nud = list(2)+local bi, nb = list(2)+local si, ns = list(3)+local given = {}+for j = 0, nb - 1 do given[ARGV[bi + 2 * j]] = true end+local missing = {'missing'}+for j = 0, nta - 1 do+  local t = ARGV[ta + 2 * j]+  if not given[t] and redis.call('EXISTS', p .. 'tx:' .. t) == 0 then+    table.insert(missing, t)+  end+end+if #missing > 1 then return missing end+for j = 0, nad - 1 do+  local a = ARGV[ad + j]+  redis.call('SREM', p .. 'a:' .. a, id)+  redis.call('SREM', xk .. 'addrs', a)+end+for j = 0, naa - 1 do+  local a = ARGV[aa + j]+  redis.call('SADD', p .. 'a:' .. a, id)+  redis.call('SADD', xk .. 'addrs', a)+end+for j = 0, ntd - 1 do+  local t = ARGV[td + j]+  local rk = p .. 'txref:' .. t+  redis.call('ZREM', xk .. 'txs', t)+  redis.call('SREM', rk, id)+  if redis.call('SCARD', rk) == 0 then+    redis.call('DEL', rk, p .. 'tx:' .. t, p .. 'sp:' .. t)+  end+end+for j = 0, nta - 1 do+  local t = ARGV[ta + 2 * j]+  redis.call('ZADD', xk .. 'txs', ARGV[ta + 2 * j + 1], t)+  redis.call('SADD', p .. 'txref:' .. t, id)+end+-- unspent outputs by outpoint: uop maps each to its member, which changes+-- with the block of the output, so that one can go by its outpoint alone+local uop = xk .. 'uop'+for j = 0, nud - 1 do+  local op, m = ARGV[ud + 2 * j], ARGV[ud + 2 * j + 1]+  if m == '' then m = redis.call('HGET', uop, op) end+  if m then redis.call('ZREM', xk .. 'utxo', m) end+  redis.call('HDEL', uop, op)+end+for j = 0, nua - 1 do+  local op, m = ARGV[ua + 3 * j], ARGV[ua + 3 * j + 1]+  local old = redis.call('HGET', uop, op)+  if old and old ~= m then redis.call('ZREM', xk .. 'utxo', old) end+  redis.call('ZADD', xk .. 'utxo', ARGV[ua + 3 * j + 2], m)+  redis.call('HSET', uop, op, m)+end+for j = 0, nb - 1 do+  local t = ARGV[bi + 2 * j]+  redis.call('SET', p .. 'tx:' .. t, ARGV[bi + 2 * j + 1])+  redis.call('DEL', p .. 'sp:' .. t)+end+for j = 0, ns - 1 do+  local k = si + 3 * j+  if given[ARGV[k]] then+    redis.call('HSET', p .. 'sp:' .. ARGV[k], ARGV[k + 1], ARGV[k + 2])+  end+end+redis.call('SET', xk .. 'bal', ARGV[6])+redis.call('HSET', meta, 'spec', ARGV[5], 'height', ARGV[3], 'ver', '3')+if full ~= '' then redis.call('HSET', meta, 'full', full) end+redis.call('HINCRBY', meta, 'gen', 1)+redis.call('ZADD', p .. 'lru', ARGV[4], id)+return {'ok'}
+ src/Haskoin/Store/Cache/lua/delete.lua view
@@ -0,0 +1,18 @@+-- Delete an xpub: deleteScript in Haskoin.Store.Cache. evict.lua removes+-- xpubs the same way.+local p = ARGV[1]+local id = ARGV[2]+local xk = p .. 'x:' .. id .. ':'+for _, a in ipairs(redis.call('SMEMBERS', xk .. 'addrs')) do+  redis.call('SREM', p .. 'a:' .. a, id)+end+for _, t in ipairs(redis.call('ZRANGE', xk .. 'txs', 0, -1)) do+  local rk = p .. 'txref:' .. t+  redis.call('SREM', rk, id)+  if redis.call('SCARD', rk) == 0 then+    redis.call('DEL', rk, p .. 'tx:' .. t, p .. 'sp:' .. t)+  end+end+local existed = redis.call('DEL', xk .. 'meta', xk .. 'bal', xk .. 'txs', xk .. 'utxo', xk .. 'addrs', xk .. 'uop')+redis.call('ZREM', p .. 'lru', id)+if existed > 0 then return 1 else return 0 end
+ src/Haskoin/Store/Cache/lua/evict.lua view
@@ -0,0 +1,23 @@+-- Evict the least recently used xpubs past a number: evictScript in+-- Haskoin.Store.Cache. Each xpub goes as delete.lua removes one.+local p = ARGV[1]+local max = tonumber(ARGV[2])+local n = 0+while redis.call('ZCARD', p .. 'lru') > max do+  local id = redis.call('ZRANGE', p .. 'lru', 0, 0)[1]+  local xk = p .. 'x:' .. id .. ':'+  for _, a in ipairs(redis.call('SMEMBERS', xk .. 'addrs')) do+    redis.call('SREM', p .. 'a:' .. a, id)+  end+  for _, t in ipairs(redis.call('ZRANGE', xk .. 'txs', 0, -1)) do+    local rk = p .. 'txref:' .. t+    redis.call('SREM', rk, id)+    if redis.call('SCARD', rk) == 0 then+      redis.call('DEL', rk, p .. 'tx:' .. t, p .. 'sp:' .. t)+    end+  end+  redis.call('DEL', xk .. 'meta', xk .. 'bal', xk .. 'txs', xk .. 'utxo', xk .. 'addrs', xk .. 'uop')+  redis.call('ZREM', p .. 'lru', id)+  n = n + 1+end+return n
+ src/Haskoin/Store/Cache/lua/page.lua view
@@ -0,0 +1,39 @@+-- A page of a sorted set of a cached xpub: pageScript in Haskoin.Store.Cache.+local set, txs = ARGV[2], ARGV[3]+if redis.call('EXISTS', ARGV[1]) == 0 then return {'none'} end+if ARGV[5] ~= '' then redis.call('ZADD', ARGV[4], ARGV[5], ARGV[6]) end+local off = tonumber(ARGV[7])+local lim = tonumber(ARGV[8])+local mode = ARGV[9]+local function lt(a, b)+  for k = 1, math.min(#a, #b) do+    local x, y = string.byte(a, k), string.byte(b, k)+    if x ~= y then return x < y end+  end+  return #a < #b+end+local function out(r)+  local o = {'ok'}+  for _, v in ipairs(r) do table.insert(o, v) end+  return o+end+if mode == 'top' then+  local stop = -1+  if lim > 0 then stop = off + lim - 1 end+  return out(redis.call('ZRANGE', set, off, stop, 'WITHSCORES'))+end+local score = ARGV[10]+local skip = 0+if mode == 'tx' then+  score = redis.call('ZSCORE', txs, ARGV[10])+  if not score then return {'nostart'} end+end+if mode ~= 'score' then+  local cut, bytes = tonumber(ARGV[11]), ARGV[12]+  for _, m in ipairs(redis.call('ZRANGEBYSCORE', set, score, score)) do+    if lt(string.sub(m, cut + 1), bytes) then skip = skip + 1 end+  end+end+local count = -1+if lim > 0 then count = lim end+return out(redis.call('ZRANGEBYSCORE', set, score, '+inf', 'WITHSCORES', 'LIMIT', off + skip, count))
+ src/Haskoin/Store/Cache/lua/spends.lua view
@@ -0,0 +1,20 @@+-- Record or forget spenders of outputs of cached transactions: spendsScript in Haskoin.Store.Cache.+local p = ARGV[1]+local n = tonumber(ARGV[2])+local i = 3+for j = 1, n do+  local t = ARGV[i]+  if redis.call('EXISTS', p .. 'tx:' .. t) == 1 then+    local sk = p .. 'sp:' .. t+    if ARGV[i + 2] ~= '' then+      redis.call('HSET', sk, ARGV[i + 1], ARGV[i + 2])+    else+      local cur = redis.call('HGET', sk, ARGV[i + 1])+      if cur and string.sub(cur, 1, 32) == ARGV[i + 3] then+        redis.call('HDEL', sk, ARGV[i + 1])+      end+    end+  end+  i = i + 4+end+return 1
+ src/Haskoin/Store/Cache/lua/unlock.lua view
@@ -0,0 +1,5 @@+-- Release a lock if it still holds the given token: unlockScript in Haskoin.Store.Cache.+if redis.call('GET', KEYS[1]) == ARGV[1] then+  return redis.call('DEL', KEYS[1])+end+return 0
src/Haskoin/Store/Common.hs view
@@ -1,12 +1,4 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}  -- | -- Module      : Haskoin.Store.Common@@ -18,22 +10,15 @@ -- -- Interfaces shared by every part of the store. ----- 'StoreReadBase' and 'StoreReadExtra' abstract over anything that can answer--- queries about blocks, transactions, addresses and extended public keys: the--- RocksDB database ("Haskoin.Store.Database.Reader"), the in-memory write--- buffer used while importing ("Haskoin.Store.Database.Writer"), and the Redis--- cache ("Haskoin.Store.Cache"). 'StoreWrite' is implemented by the writer.+-- 'StoreRead' is the queries that the web server asks about blocks,+-- transactions, addresses and extended public keys, as a record of functions+-- in 'IO': "Haskoin.Store.Database.Query" gives them for the RocksDB+-- database, and the web server puts the Redis cache in front of some. -- -- This module also has helpers for pagination ('Limits'), for deriving--- addresses from extended public keys, and for merging sorted streams.--- = Why the store is a set of type classes------ The same questions are asked in three places: by the web server against the--- database, by the import logic against a buffer of uncommitted changes, and by--- the cache, which answers some questions itself and passes others on. Writing--- the queries once against a class, and providing instances for each place, keeps--- those three in agreement and lets the cache sit transparently in front of the--- database.+-- addresses from extended public keys ('scanXPubBals'), for the order of+-- transactions and outputs ('txRefKey', 'unspentKey'), and for merging sorted+-- streams. -- -- = Why pages start at a transaction --@@ -44,12 +29,11 @@ module Haskoin.Store.Common   ( Limits (..),     Start (..),-    StoreReadBase (..),-    StoreReadExtra (..),-    StoreWrite (..),+    StoreRead (..),     StoreEvent (..),     getActiveBlock,     getActiveTxData,+    notDeleted,     getDefaultBalance,     getTransaction,     getTransactions,@@ -60,6 +44,9 @@     xPubSummary,     deriveAddresses,     deriveFunction,+    scanXPubBals,+    txRefKey,+    unspentKey,     deOffset,     applyLimits,     applyLimitsC,@@ -86,18 +73,18 @@     ($$++),   ) import Control.Monad.Trans (lift)-import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import Control.Monad.Trans.Reader (runReaderT) import Data.ByteString (ByteString) import Data.Default (Default (..))+import Data.Functor ((<&>)) import Data.Graph qualified as G import Data.HashMap.Strict qualified as M import Data.HashSet qualified as H import Data.Hashable (Hashable)-import Data.Map.Strict qualified as Map import Data.List (sortOn)-import Data.Maybe (catMaybes, mapMaybe)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, fromMaybe, mapMaybe) import Data.Ord (Down (..))+import Data.Serialize (encode) import Data.Time.Clock.System   ( getSystemTime,     systemNanoseconds,@@ -105,10 +92,11 @@   ) import Data.Word (Word32, Word64) import Haskoin-import Haskoin.Node (Chain, Peer)+import Haskoin.Node (Chain, Peer, chainRunHeaders) import Haskoin.Store.Data   ( Balance (..),     BlockData (..),+    BlockRef (..),     DeriveType (..),     Spender,     Transaction (..),@@ -124,7 +112,6 @@     toTransaction,     zeroBalance,   )-import UnliftIO (MonadIO, liftIO)  -- | Function to derive the address at an index of an extended public key. type DeriveAddr = XPubKey -> KeyIndex -> Address@@ -141,8 +128,9 @@     -- entered the mempool, and for a confirmed one, at its position in the     -- chain     AtTx !TxHash-  | -- | at this block height, including every transaction in the mempool if it-    -- is above the best block+  | -- | at the last transaction confirmed at or below this block height.+    -- Nothing of the mempool is in the page, not even when the height is+    -- above the best block     AtBlock !BlockHeight   deriving (Eq, Show) @@ -165,131 +153,117 @@ instance Default Limits where   def = defaultLimits --- | Basic queries that every store answers.-class (Monad m) => StoreReadBase m where-  -- | Network the store indexes.-  getNetwork :: m Network-  -- | Context for secp256k1 operations, needed to parse and derive keys.-  getCtx :: m Ctx-  -- | Hash of the best block imported, or 'Nothing' if none was imported yet.-  getBestBlock :: m (Maybe BlockHash)-  -- | Hashes of all blocks imported at a height, including those on side-  -- chains.-  getBlocksAtHeight :: BlockHeight -> m [BlockHash]-  -- | Data of a block imported in the past, whether still in the main chain-  -- or not. Check 'Haskoin.Store.Data.main' to know.-  getBlock :: BlockHash -> m (Maybe BlockData)-  -- | Data of a transaction, including deleted ones. Check-  -- 'Haskoin.Store.Data.deleted' to know.-  getTxData :: TxHash -> m (Maybe TxData)--  -- | Data of many transactions, in the order of their hashes. Stores that-  -- can read many keys at once read them together.-  getManyTxData :: [TxHash] -> m [Maybe TxData]-  getManyTxData = mapM getTxData--  -- | Transaction input that spends an output, if it is spent.-  getSpender :: OutPoint -> m (Maybe Spender)-  -- | Balance of an address, or 'Nothing' if it was never used.-  getBalance :: Address -> m (Maybe Balance)-  -- | Unspent output, or 'Nothing' if it does not exist or is spent.-  getUnspent :: OutPoint -> m (Maybe Unspent)-  -- | Whole mempool, newest first.-  getMempool :: m [(UnixTime, TxHash)]--  -- | Page of the mempool, newest first, from the offset and limit of the-  -- 'Limits'; its start is ignored. Stores that index the mempool by time-  -- read only the entries of the page instead of the whole mempool.-  getMempoolPage :: Limits -> m [(UnixTime, TxHash)]-  getMempoolPage l = applyLimits l <$> getMempool---- | Queries about addresses and extended public keys, which need indices or--- configuration beyond the basic store.-class (StoreReadBase m) => StoreReadExtra m where-  -- | Transactions involving any of the addresses, newest first, without-  -- duplicates.-  getAddressesTxs :: [Address] -> Limits -> m [TxRef]-  -- | Unspent outputs of any of the addresses, newest first.-  getAddressesUnspents :: [Address] -> Limits -> m [Unspent]-  -- | Number of unused addresses to look ahead when deriving the addresses of-  -- an extended public key that has no transactions yet.-  getInitialGap :: m Word32-  -- | Number of consecutive unused addresses after which derivation of an-  -- extended public key stops.-  getMaxGap :: m Word32-  -- | Transactions whose hash, read as a number, matches the short identifier-  -- used by the Blockchain.info compatibility API. There may be more than one.-  getNumTxData :: Word64 -> m [TxData]-  -- | Balances of the addresses, in the same order. Addresses never used get-  -- a zero balance.-  getBalances :: [Address] -> m [Balance]-  -- | Transactions involving an address, newest first.-  getAddressTxs :: Address -> Limits -> m [TxRef]-  -- | Unspent outputs of an address, newest first.-  getAddressUnspents :: Address -> Limits -> m [Unspent]-  -- | Balances of the used addresses of an extended public key, with their-  -- derivation paths.-  xPubBals :: XPubSpec -> m [XPubBal]-  -- | Unspent outputs of an extended public key, newest first. The second-  -- argument is the result of 'xPubBals' for the same key.-  xPubUnspents :: XPubSpec -> [XPubBal] -> Limits -> m [XPubUnspent]-  -- | Transactions of an extended public key, newest first. The second-  -- argument is the result of 'xPubBals' for the same key.-  xPubTxs :: XPubSpec -> [XPubBal] -> Limits -> m [TxRef]-  -- | Number of transactions of an extended public key. The second argument-  -- is the result of 'xPubBals' for the same key.-  xPubTxCount :: XPubSpec -> [XPubBal] -> m Word32---- | Changes to the index. Implemented by the writer, which buffers them in--- memory and commits them to the database in one batch.-class StoreWrite m where-  -- | Set the best block.-  setBest :: BlockHash -> m ()-  -- | Add or replace the data of a block.-  insertBlock :: BlockData -> m ()-  -- | Set the hashes of the blocks at a height.-  setBlocksAtHeight :: [BlockHash] -> BlockHeight -> m ()-  -- | Add or replace the data of a transaction.-  insertTx :: TxData -> m ()-  -- | Add a transaction to the index of an address.-  insertAddrTx :: Address -> TxRef -> m ()-  -- | Remove a transaction from the index of an address.-  deleteAddrTx :: Address -> TxRef -> m ()-  -- | Add an unspent output to the index of an address.-  insertAddrUnspent :: Address -> Unspent -> m ()-  -- | Remove an unspent output from the index of an address.-  deleteAddrUnspent :: Address -> Unspent -> m ()-  -- | Add a transaction to the mempool, with the time it arrived.-  addToMempool :: TxHash -> UnixTime -> m ()-  -- | Remove a transaction from the mempool.-  deleteFromMempool :: TxHash -> m ()-  -- | Set the balance of an address.-  setBalance :: Balance -> m ()-  -- | Add an unspent output.-  insertUnspent :: Unspent -> m ()-  -- | Remove an unspent output.-  deleteUnspent :: OutPoint -> m ()+-- | The queries that the web server answers with, as plain functions in+-- 'IO'. "Haskoin.Store.Database.Query" gives them for the database+-- ('Haskoin.Store.Database.Query.databaseRead'); the web server puts the+-- Redis cache in front of some of them.+--+-- A record of functions rather than a class: a class needs a monad to pick+-- its instance, and the store has no monads of its own to pick it with, only+-- 'IO'. The record makes the source of every answer an ordinary value that+-- can be passed around and replaced, as @nocache=true@ does.+data StoreRead = StoreRead+  { -- | network the store indexes+    net :: !Network,+    -- | context for secp256k1 operations, needed to parse and derive keys+    ctx :: !Ctx,+    -- | hash of the best block imported, or 'Nothing' if none yet+    getBestBlock :: IO (Maybe BlockHash),+    -- | hashes of all blocks imported at a height, side chains included+    getBlocksAtHeight :: BlockHeight -> IO [BlockHash],+    -- | data of a block imported in the past, whether still in the main+    -- chain or not; check 'Haskoin.Store.Data.main' to know+    getBlock :: BlockHash -> IO (Maybe BlockData),+    -- | data of a transaction that is confirmed or in the mempool. One+    -- that was deleted is not returned, here or by the two queries below;+    -- see 'notDeleted'+    getTxData :: TxHash -> IO (Maybe TxData),+    -- | data of many transactions, in the order of their hashes, read+    -- together+    getManyTxData :: [TxHash] -> IO [Maybe TxData],+    -- | transaction input that spends an output, if it is spent+    getSpender :: OutPoint -> IO (Maybe Spender),+    -- | balance of an address, or 'Nothing' if it was never used+    getBalance :: Address -> IO (Maybe Balance),+    -- | unspent output, or 'Nothing' if it does not exist or is spent+    getUnspent :: OutPoint -> IO (Maybe Unspent),+    -- | whole mempool, newest first+    getMempool :: IO [(UnixTime, TxHash)],+    -- | page of the mempool, newest first, from the offset and limit of the+    -- 'Limits'; its start is ignored+    getMempoolPage :: Limits -> IO [(UnixTime, TxHash)],+    -- | transactions involving any of the addresses, newest first, without+    -- duplicates+    getAddressesTxs :: [Address] -> Limits -> IO [TxRef],+    -- | unspent outputs of any of the addresses, newest first+    getAddressesUnspents :: [Address] -> Limits -> IO [Unspent],+    -- | unused addresses to look at for an extended public key with no+    -- transactions yet+    initialGap :: !Word32,+    -- | consecutive unused addresses after which derivation of an extended+    -- public key stops+    maxGap :: !Word32,+    -- | transactions whose hash, read as a number, matches the short+    -- identifier of the Blockchain.info compatible API; there may be more+    -- than one+    getNumTxData :: Word64 -> IO [TxData],+    -- | balances of the addresses, in the same order; addresses never used+    -- get a zero balance+    getBalances :: [Address] -> IO [Balance],+    -- | transactions involving an address, newest first+    getAddressTxs :: Address -> Limits -> IO [TxRef],+    -- | unspent outputs of an address, newest first+    getAddressUnspents :: Address -> Limits -> IO [Unspent],+    -- | balances of the used addresses of an extended public key, with+    -- their derivation paths+    xPubBals :: XPubSpec -> IO [XPubBal],+    -- | unspent outputs of an extended public key, newest first, given its+    -- balances from 'xPubBals'+    xPubUnspents :: XPubSpec -> [XPubBal] -> Limits -> IO [XPubUnspent],+    -- | transactions of an extended public key, newest first, given its+    -- balances from 'xPubBals'+    xPubTxs :: XPubSpec -> [XPubBal] -> Limits -> IO [TxRef],+    -- | number of transactions of an extended public key, given its+    -- balances from 'xPubBals'+    xPubTxCount :: XPubSpec -> [XPubBal] -> IO Word32,+    -- | number of distinct transactions of a set of addresses, counted+    -- without reading them+    getAddressesTxCount :: [Address] -> IO Word32+  }  -- | Data of a block, or 'Nothing' if it is unknown or not in the main chain.-getActiveBlock :: (StoreReadExtra m) => BlockHash -> m (Maybe BlockData)-getActiveBlock bh =-  getBlock bh >>= \case-    Just b | b.main -> return (Just b)-    _ -> return Nothing+getActiveBlock :: StoreRead -> BlockHash -> IO (Maybe BlockData)+getActiveBlock r bh =+  r.getBlock bh <&> \case+    Just b | b.main -> Just b+    _ -> Nothing +-- | Leave out a transaction that was deleted: replaced, dropped from the+-- mempool, or the coinbase of a block that left the main chain.+--+-- The store used to answer with such transactions, marked as deleted, so+-- that a wallet could say what became of a payment. Some clients do not+-- look at the mark, and took a payment that will never happen for one that+-- is pending. So every 'StoreRead' leaves them out, and to the API a+-- deleted transaction is one that the store does not know. Since database+-- version 22 nothing is kept of them at all; this is for the bodies that+-- version 20 left in a database that was migrated.+notDeleted :: Maybe TxData -> Maybe TxData+notDeleted (Just td) | td.deleted = Nothing+notDeleted found = found+ -- | Data of a transaction, or 'Nothing' if it is unknown or was deleted.-getActiveTxData :: (StoreReadBase m) => TxHash -> m (Maybe TxData)-getActiveTxData th =-  getTxData th >>= \case-    Just td | not td.deleted -> return (Just td)-    _ -> return Nothing+-- The same as the query of a 'StoreRead' now that it leaves deleted+-- transactions out.+getActiveTxData :: StoreRead -> TxHash -> IO (Maybe TxData)+getActiveTxData r th =+  r.getTxData th <&> \case+    Just td | not td.deleted -> Just td+    _ -> Nothing  -- | Balance of an address, or a zero balance if the address was never used.-getDefaultBalance :: (StoreReadBase m) => Address -> m Balance-getDefaultBalance a =-  getBalance a >>= \case-    Nothing -> return $ zeroBalance a-    Just b -> return b+getDefaultBalance :: StoreRead -> Address -> IO Balance+getDefaultBalance r a = fromMaybe (zeroBalance a) <$> r.getBalance a  -- | Infinite list of the addresses of an extended public key, with their -- indices, from the given index on.@@ -304,6 +278,76 @@ deriveFunction ctx DeriveP2SH i = fst . deriveCompatWitnessAddr ctx i deriveFunction ctx DeriveP2WPKH i = fst . deriveWitnessAddr ctx i +-- | Balances of the addresses of an extended public key, derived up to the+-- gap: every address of each chain up to the last used one, followed by+-- exactly @gap@ unused ones. Arguments, in order: a function to read the+-- balances of addresses, the initial gap, the gap, the external chain and+-- the change chain (both infinite lists of indices and addresses).+--+-- The first @initial gap@ external addresses are read first. If none is used,+-- the key is taken as unused, those balances are returned and nothing more is+-- read. Otherwise each chain is read until the last @gap@ addresses are+-- unused, counting the unused addresses at the end of the initial block too.+-- The initial block is always returned whole, even when it reaches past the+-- gap after its last used address (an initial gap larger than the gap): it+-- was read, so a payment to any of its addresses is seen, and the cache must+-- keep those addresses to see it too.+--+-- = Why the gap is exact+--+-- The gap is the number of unused addresses a wallet may hand out in a row.+-- Wallets follow BIP 44 and stop looking for payments after that many, so a+-- store that looked further would show payments the wallet cannot see, and a+-- store that sometimes looked further and sometimes not would make the answer+-- depend on how the reads happened to be batched. Earlier versions read 32+-- addresses at a time and checked the gap only between batches, so they+-- looked between 33 and 64 addresses past the last used one, and started+-- counting again at the end of the initial block. The Redis cache keeps+-- exactly @gap@ unused addresses, so cached and uncached answers differed+-- whenever a payment fell in the extra range.+--+-- Reads are still batched: each read asks for as many addresses as are still+-- needed to complete the gap if all of them turn out unused, so no address+-- past the gap is ever read, and a run of unused addresses costs one read.+scanXPubBals ::+  (Monad m) =>+  ([Address] -> m [Balance]) ->+  Word32 ->+  Word32 ->+  [(Word32, Address)] ->+  [(Word32, Address)] ->+  m [XPubBal]+scanXPubBals getBals igap gap ext chg = do+  let (first, rest) = splitAt (fromIntegral igap) ext+  ext1 <- readChain 0 first+  if all nullxb ext1+    then return ext1+    else do+      ext2 <- go (length ext1) 0 (reverse ext1) rest+      chg' <- go 0 1 [] chg+      return (ext2 <> chg')+  where+    g = fromIntegral gap :: Int+    nullxb b = nullBalance b.balance+    readChain m as = do+      bs <- getBals (map snd as)+      return $ zipWith (\(n, _) b -> XPubBal {path = [m, n], balance = b}) as bs+    -- acc holds the balances read so far, newest first; keep is the number+    -- of addresses at the start of the chain to return in any case+    trailing = length . takeWhile nullxb+    go keep m acc as+      | trailing acc >= g = return (trim keep acc)+      | otherwise =+          case splitAt (g - trailing acc) as of+            ([], _) -> return (trim keep acc)+            (now, later) -> do+              bs <- readChain m now+              go keep m (reverse bs <> acc) later+    trim keep acc =+      let (ns, ys) = span nullxb acc+          gapped = reverse ys <> take g (reverse ns)+       in if length gapped >= keep then gapped else take keep (reverse acc)+ -- | Summary of the balances of an extended public key, from the balances of its -- addresses. The received total only counts external addresses, and the -- indices are those of the last used external and change addresses.@@ -326,73 +370,52 @@     rx = sum [b.balance.received | b <- xt]  -- | Transaction with its metadata, or 'Nothing' if it is unknown.-getTransaction ::-  (StoreReadBase m) => TxHash -> m (Maybe Transaction)-getTransaction h = do-  net <- getNetwork-  ctx <- getCtx-  fmap (toTransaction net ctx) <$> getTxData h+getTransaction :: StoreRead -> TxHash -> IO (Maybe Transaction)+getTransaction r h = fmap (toTransaction r.net r.ctx) <$> r.getTxData h  -- | Many transactions, in the order of their hashes, read together.-getTransactions ::-  (StoreReadBase m) => [TxHash] -> m [Maybe Transaction]-getTransactions hs = do-  net <- getNetwork-  ctx <- getCtx-  map (fmap (toTransaction net ctx)) <$> getManyTxData hs+getTransactions :: StoreRead -> [TxHash] -> IO [Maybe Transaction]+getTransactions r hs = map (fmap (toTransaction r.net r.ctx)) <$> r.getManyTxData hs  -- | Transactions that match a short numeric identifier; see 'getNumTxData'.-getNumTransaction ::-  (StoreReadExtra m) => Word64 -> m [Transaction]-getNumTransaction i = do-  net <- getNetwork-  ctx <- getCtx-  map (toTransaction net ctx) <$> getNumTxData i+getNumTransaction :: StoreRead -> Word64 -> IO [Transaction]+getNumTransaction r i = map (toTransaction r.net r.ctx) <$> r.getNumTxData i  -- | First block in the main chain whose timestamp is at or after the given time, -- or 'Nothing' if there is none.-blockAtOrAfter ::-  (MonadIO m, StoreReadExtra m) =>-  Chain ->-  UnixTime ->-  m (Maybe BlockData)-blockAtOrAfter ch q = runMaybeT $ do-  net <- lift getNetwork-  x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch-  MaybeT $ getBlock $ headerHash x.header+blockAtOrAfter :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)+blockAtOrAfter r ch q =+  chainRunHeaders ch (firstGreaterOrEqual r.net f) >>= \case+    Nothing -> return Nothing+    Just x -> r.getBlock (headerHash x.header)   where+    f :: (BlockHeaders n) => BlockNode -> n Ordering     f x =       let t = fromIntegral x.header.timestamp        in return $ t `compare` q  -- | Last block in the main chain whose timestamp is at or before the given time, -- or 'Nothing' if there is none.-blockAtOrBefore ::-  (MonadIO m, StoreReadExtra m) =>-  Chain ->-  UnixTime ->-  m (Maybe BlockData)-blockAtOrBefore ch q = runMaybeT $ do-  net <- lift getNetwork-  x <- MaybeT $ liftIO $ runReaderT (lastSmallerOrEqual net f) ch-  MaybeT $ getBlock $ headerHash x.header+blockAtOrBefore :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)+blockAtOrBefore r ch q =+  chainRunHeaders ch (lastSmallerOrEqual r.net f) >>= \case+    Nothing -> return Nothing+    Just x -> r.getBlock (headerHash x.header)   where+    f :: (BlockHeaders n) => BlockNode -> n Ordering     f x =       let t = fromIntegral x.header.timestamp        in return $ t `compare` q  -- | First block in the main chain whose median time past is at or after the given -- time, or 'Nothing' if there is none.-blockAtOrAfterMTP ::-  (MonadIO m, StoreReadExtra m) =>-  Chain ->-  UnixTime ->-  m (Maybe BlockData)-blockAtOrAfterMTP ch q = runMaybeT $ do-  net <- lift getNetwork-  x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch-  MaybeT $ getBlock $ headerHash x.header+blockAtOrAfterMTP :: StoreRead -> Chain -> UnixTime -> IO (Maybe BlockData)+blockAtOrAfterMTP r ch q =+  chainRunHeaders ch (firstGreaterOrEqual r.net f) >>= \case+    Nothing -> return Nothing+    Just x -> r.getBlock (headerHash x.header)   where+    f :: (BlockHeaders n) => BlockNode -> n Ordering     f x = do       t <- fromIntegral <$> mtp x       return $ t `compare` q@@ -478,8 +501,8 @@ nub' :: (Hashable a) => [a] -> [a] nub' = H.toList . H.fromList --- | A 'forever' that stays a flat loop in any monad. 'forever' sequences--- with @*>@; monad-logger's @LoggingT@ leaves @*>@ at its default+-- | A 'Control.Monad.forever' that stays a flat loop in any monad.+-- 'Control.Monad.forever' sequences with @*>@; monad-logger's @LoggingT@ leaves @*>@ at its default -- @(id <$ a) <*> b@ and @ReaderT@ defines @>>@ as @*>@, so a loop that is -- polymorphic in its monad and not specialised by GHC keeps an @<*>@ -- continuation frame and two thunks per iteration for its whole lifetime.@@ -491,12 +514,12 @@ {-# INLINE loopForever #-}  -- | Current time in microseconds since the Unix epoch.-microseconds :: (MonadIO m) => m Integer+microseconds :: IO Integer microseconds =   let f t =         toInteger (systemSeconds t) * 1000000           + toInteger (systemNanoseconds t) `div` 1000-   in liftIO $ f <$> getSystemTime+   in f <$> getSystemTime  -- | Stream every result of a paged query, fetching one page at a time. --@@ -536,48 +559,74 @@           [] -> return ()           ls -> mapM_ yield ls >> go l' (last ls) --- | Merge streams sorted in decreasing order into one stream sorted in decreasing--- order, dropping duplicates.+-- | Merge streams sorted in decreasing order of a key into one stream sorted+-- in decreasing order of that key, dropping items whose key is the same as+-- the one before. The key must be the order the streams are really sorted+-- in: an item that comes out of order in its stream can be emitted twice.+--+-- Use 'txRefKey' for transactions and 'unspentKey' for unspent outputs: they+-- are the order the database and the cache both keep, including the order of+-- ties between mempool transactions that arrived in the same second. joinDescStreams ::-  (Monad m, Ord a) =>+  (Monad m, Ord k) =>+  (a -> k) ->   [ConduitT () a m ()] ->   ConduitT () a m ()-joinDescStreams xs = do+joinDescStreams key xs = do   let ss = map sealConduitT xs   go Nothing =<< g ss   where-    j (x, y) = (,[x]) <$> y+    j (x, y) = (\v -> (key v, [(v, x)])) <$> y     g ss =       let l = mapMaybe j <$> lift (traverse ($$++ await) ss)        in Map.fromListWith (++) <$> l     go m mp = case Map.lookupMax mp of       Nothing -> return ()-      Just (x, ss) -> do+      Just (k, vss) -> do         case m of-          Nothing -> yield x-          Just x'-            | x == x' -> return ()-            | otherwise -> yield x-        mp1 <- g ss+          Just k' | k == k' -> return ()+          _ -> yield (fst (last vss))+        mp1 <- g (map snd vss)         let mp2 = Map.deleteMax mp             mp' = Map.unionWith (++) mp1 mp2-        go (Just x) mp'+        go (Just k) mp' +-- | Order of transaction references, newest first when sorted in decreasing+-- order: by block position, and between mempool transactions that arrived in+-- the same second, by the bytes of their hashes in increasing order.+--+-- The tie order is not the derived 'Ord' of 'TxRef', which would sort the+-- hashes in decreasing order. It is the order of the database keys, which+-- serialize the hash after the block position, and of the Redis sorted sets+-- of the cache, which break ties between equal scores by the bytes of the+-- member. Using it everywhere makes every source agree, so that merged pages+-- have no duplicates and the cache answers exactly like the database.+txRefKey :: TxRef -> (BlockRef, Down TxHash)+txRefKey t = (t.block, Down t.txid)++-- | Order of unspent outputs, newest first when sorted in decreasing order:+-- by block position, then by the serialized output point in increasing+-- order. See 'txRefKey' for why the ties are broken by serialized bytes.+unspentKey :: Unspent -> (BlockRef, Down ByteString)+unspentKey u = (u.block, Down (encode u.outpoint))+ -- | Merge streams whose keys never increase, taking at most the given number -- of items from each stream ('Nothing': all of them). The items come out in -- groups of equal key, in decreasing order of key, and each group is sorted--- in decreasing order of the items themselves. So the output is the same as+-- in decreasing order of the second function, which must agree with the+-- first. So the output is the same as -- sorting the first items of every stream in decreasing order, but a stream -- is read only as far as the consumer needs: a consumer that stops after -- the first n items makes each stream be read only a little past its first -- n items, instead of to the end of its quota. mergeDescGroups ::-  (Monad m, Ord a, Ord k) =>+  (Monad m, Ord o, Ord k) =>   (a -> k) ->+  (a -> o) ->   Maybe Word32 ->   [ConduitT () a m ()] ->   ConduitT () a m ()-mergeDescGroups key quota streams = do+mergeDescGroups key order quota streams = do   heads <- lift $ mapM (next quota . sealConduitT) streams   go (insertAll (catMaybes heads) Map.empty)   where@@ -601,5 +650,5 @@       Nothing -> return ()       Just ((Down k, hs), mp') -> do         gs <- lift $ mapM (group k) hs-        mapM_ yield (sortOn Down (concatMap fst gs))+        mapM_ yield (sortOn (Down . order) (concatMap fst gs))         go (insertAll (mapMaybe snd gs) mp')
+ src/Haskoin/Store/Database/Mempool.hs view
@@ -0,0 +1,505 @@+-- |+-- Module      : Haskoin.Store.Database.Mempool+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The mempool area of the layered layout of the database, and how it is+-- applied on top of the chain. The design, and the reasons for it, are in+-- @docs\/layered-database.md@. This is the layout that the store runs on,+-- since version 21. Its rules are checked by the tests of+-- "Haskoin.Store.Database.Migrate" against the answers of a database of+-- version 20, in which the mempool was mixed into the chain. The queries+-- that apply them are in "Haskoin.Store.Database.Query.Layered".+--+-- = Two layers+--+-- In the layered layout the column families of the chain hold only what+-- follows from the blocks of the main chain, and change only when a block is+-- imported or reverted. Everything about unconfirmed transactions is in two+-- column families of its own, 'mempoolCF' and 'mempoolAddrCF'. A query reads+-- the chain, reads what the mempool has for the same transaction, output or+-- address, and puts the two together. Emptying the mempool area leaves the+-- chain as it is, with an empty mempool on top.+--+-- = Every key belongs to one transaction+--+-- 'mempoolRecords' gives the keys that a mempool transaction contributes.+-- They depend on that transaction alone: on its inputs, with the outputs+-- they spend, which it carries with it, and on its outputs. The mempool area+-- is the union of the keys of its transactions. So a transaction enters the+-- mempool by writing its keys and leaves it by deleting them, in any order,+-- and when a block confirms some transactions and not their descendants,+-- nothing has to be worked out again for those that stay.+--+-- The alternative is to keep, for the mempool, the same values that the+-- chain has: a balance, a list of unspent outputs. Such values depend on the+-- chain under them. A balance with the mempool applied is wrong as soon as a+-- block pays the address, and every block would have to find and correct+-- the values it made stale. The keys here say what the mempool does, not+-- what the result is, so they stay right whatever happens to the chain.+--+-- What a query needs is worked out when it is answered, from the few entries+-- that the mempool has for an address, an t'AddressMempool':+--+-- * an output that the mempool creates and does not spend is an unspent+--   output of the mempool ('mempoolUnspents');+--+-- * an output that the mempool spends and did not create belongs to the+--   chain, which still has it as unspent, and is left out of what the chain+--   says ('hiddenOutputs');+--+-- * an output that the mempool both creates and spends shows up in neither,+--   and only counts towards what the address received.+--+-- = The summary of an address+--+-- The entries of an address are few for nearly every address, and many+-- thousands for some. So each address that the mempool touches also has a+-- t'MemSummary', one key with what its entries add up to ('summarise'). A+-- balance is the balance of the chain and the summary ('applySummary'), and+-- the queries read the entries themselves only for lists, as far as a page+-- goes.+--+-- The summary is not the key of one transaction, but it is a function of+-- the entries of its address and of nothing else, so it shares what makes+-- them safe: nothing that happens to the chain makes it wrong.+-- 'wrongSummaries' checks that, and 'rebuildSummaries' writes them all+-- again from the entries.+module Haskoin.Store.Database.Mempool+  ( -- * What a transaction contributes+    MemRecords (..),+    mempoolRecords,+    recordEntries,+    insertRecordOps,+    deleteRecordOps,++    -- * Reading the mempool area+    AddressMempool (..),+    getAddressMempool,+    getAddressMempools,+    getMempoolTx,+    getMempoolSpenders,+    wipeMempoolArea,++    -- * Applying the mempool on top of the chain+    mempoolUnspents,+    hiddenOutputs,+    applyMempool,+    mergeBalance,++    -- * The summary of an address+    summarise,+    summaryEntries,+    applySummary,+    summaryBalance,+    getSummaries,+    rebuildSummaries,+    wrongSummaries,+    addressArea,+  )+where++import Conduit (runConduit, sinkList, (.|))+import Data.ByteString (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IntMap+import Data.Maybe (fromMaybe, isNothing, mapMaybe)+import Data.Serialize (Serialize, decode, encode)+import Data.Word (Word32, Word64)+import Database.RocksDB+  ( BatchOp (..),+    ColumnFamily,+    DB,+    Iterator,+    getCF,+    iterFirst,+    iterKey,+    iterNext,+    multiGetCF,+    withIterCF,+  )+import Database.RocksDB.Query+  ( matching,+    matchingAsListCF,+    retrieveCF,+    writeBatch,+  )+import Haskoin+  ( Address,+    Ctx,+    Network,+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    eitherToMaybe,+    nullOutPoint,+    scriptToAddressBS,+    txHash,+  )+import Haskoin.Store.Common (nub')+import Haskoin.Store.Data+  ( Balance (..),+    BlockRef (..),+    Prev (..),+    Spender (..),+    TxData (..),+    TxRef (..),+    UnixTime,+    Unspent (..),+    zeroBalance,+  )+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    mempoolAddrCF,+    mempoolCF,+  )+import Haskoin.Store.Database.Types+  ( MemAddrKey (..),+    MemAddrVal (..),+    MemSpenderKey (..),+    MemSummary (..),+    MemTimeKey (..),+    MemTxKey (..),+    TxKey (..),+  )++-------------------------------------------------------------------------------+-- What a transaction contributes+-------------------------------------------------------------------------------++-- | What one mempool transaction contributes to the mempool area.+data MemRecords = MemRecords+  { -- | hash of the transaction+    txid :: !TxHash,+    -- | time it entered the mempool: its place in the list of the mempool+    -- and in the index by time+    time :: !UnixTime,+    -- | its body, without spenders and at a mempool position+    body :: !TxData,+    -- | the outputs it spends, each with the input that spends it+    spends :: ![(OutPoint, Spender)],+    -- | for each address it touches: the transaction itself, the outputs of+    -- the address that it spends and the outputs that it pays to the+    -- address+    addresses :: ![(MemAddrKey, MemAddrVal)]+  }+  deriving (Eq, Show)++-- | What a mempool transaction that entered the mempool at a time+-- contributes to the mempool area.+--+-- It is a function of the transaction and that time and of nothing else:+-- not of the chain, and not of the other transactions in the mempool. The+-- body is stored without spenders, which have keys of their own, and as not+-- deleted. The flag for replace-by-fee is kept as given; it is the one+-- thing here that can depend on other transactions, because a transaction+-- inherits it from unconfirmed ancestors, and it is corrected when one of+-- them is confirmed.+mempoolRecords :: Network -> Ctx -> UnixTime -> TxData -> MemRecords+mempoolRecords net ctx time td =+  MemRecords+    { txid = h,+      time,+      body = (td :: TxData) {block = MemRef time, deleted = False, spenders = IntMap.empty},+      spends = [(op, Spender h k) | (k, op) <- spent],+      addresses =+        [(MemAddrTxKey a time h, MemAddrTxVal) | a <- touched]+          <> [ (MemAddrSpentKey a op, MemAddrSpentVal p.value)+             | (k, op) <- spent,+               Just p <- [IntMap.lookup (fromIntegral k) td.prevs],+               Just a <- [address p.script]+             ]+          <> [ (MemAddrOutKey a time (OutPoint h i), MemAddrOutVal o.value o.script)+             | (i, o) <- zip [0 ..] td.tx.outputs,+               Just a <- [address o.script]+             ]+    }+  where+    h = txHash td.tx+    spent = [(k, i.outpoint) | (k, i) <- zip [0 :: Word32 ..] td.tx.inputs, i.outpoint /= nullOutPoint]+    address = eitherToMaybe . scriptToAddressBS net ctx+    touched =+      nub' $+        mapMaybe (address . (.script)) (IntMap.elems td.prevs)+          <> mapMaybe (address . (.script)) td.tx.outputs++-- | The keys and values of a transaction, serialized, for each of the two+-- column families of the mempool area: the mempool column family first,+-- then the one of address entries.+recordEntries :: MemRecords -> ([(ByteString, ByteString)], [(ByteString, ByteString)])+recordEntries r =+  ( [ kv (MemTxKey r.txid) r.time,+      kv (MemTimeKey r.time r.txid) (),+      kv (TxKey r.txid) r.body+    ]+      <> [kv (MemSpenderKey op) s | (op, s) <- r.spends],+    [kv k v | (k, v) <- r.addresses]+  )+  where+    kv :: (Serialize k, Serialize v) => k -> v -> (ByteString, ByteString)+    kv k v = (encode k, encode v)++-- | Operations that write the keys of a transaction to the mempool area.+insertRecordOps :: DB -> MemRecords -> [BatchOp]+insertRecordOps db r =+  [PutCF (mempoolCF db) k v | (k, v) <- general]+    <> [PutCF (mempoolAddrCF db) k v | (k, v) <- byAddress]+  where+    (general, byAddress) = recordEntries r++-- | Operations that delete the keys of a transaction from the mempool area.+deleteRecordOps :: DB -> MemRecords -> [BatchOp]+deleteRecordOps db r =+  [DelCF (mempoolCF db) k | (k, _) <- general]+    <> [DelCF (mempoolAddrCF db) k | (k, _) <- byAddress]+  where+    (general, byAddress) = recordEntries r++-------------------------------------------------------------------------------+-- Reading the mempool area+-------------------------------------------------------------------------------++-- | What the mempool does at an address. It is small, usually empty, and+-- read with one scan of the entries of the address.+data AddressMempool = AddressMempool+  { -- | mempool transactions that spend from or pay to the address, newest+    -- first, in the order of 'Haskoin.Store.Common.txRefKey'+    txs :: ![TxRef],+    -- | outputs of the address that mempool transactions spend, with their+    -- amounts, whether a block or the mempool created them+    spent :: !(HashMap OutPoint Word64),+    -- | outputs that mempool transactions pay to the address, whether the+    -- mempool spends them or not, newest first, in the order of+    -- 'Haskoin.Store.Common.unspentKey'+    created :: ![Unspent]+  }+  deriving (Eq, Show)++-- | What the mempool does at an address.+getAddressMempool :: DatabaseReader -> Address -> IO AddressMempool+getAddressMempool r a = withIterCF r.db (mempoolAddrCF r.db) (`addressMempool` a)++-- | What the mempool does at each of some addresses, in their order.+--+-- All of them are read through one iterator, which is moved to each address+-- in turn. A query about an extended public key asks about hundreds of+-- addresses, nearly all with nothing in the mempool; opening an iterator for+-- each would cost more than the reads, each of which stops at the filter.+getAddressMempools :: DatabaseReader -> [Address] -> IO [AddressMempool]+getAddressMempools _ [] = return []+getAddressMempools r as = withIterCF r.db (mempoolAddrCF r.db) $ \it -> mapM (addressMempool it) as++-- | What the mempool does at an address, read with an iterator on the+-- column family of the entries by address, which is left wherever the scan+-- ended.+addressMempool :: Iterator -> Address -> IO AddressMempool+addressMempool it a = do+  rows <- runConduit $ matching it (MemAddrKeyA a) .| sinkList+  return+    AddressMempool+      { txs = [TxRef {block = MemRef t, txid = h} | (MemAddrTxKey _ t h, _) <- rows],+        spent = HashMap.fromList [(p, v) | (MemAddrSpentKey _ p, MemAddrSpentVal v) <- rows],+        created =+          [ Unspent {block = MemRef t, outpoint = p, value = v, script = s, address = Just a}+          | (MemAddrOutKey _ t p, MemAddrOutVal v s) <- rows+          ]+      }++-- | Body of a transaction that is in the mempool, without its spenders.+getMempoolTx :: DatabaseReader -> TxHash -> IO (Maybe TxData)+getMempoolTx r h = retrieveCF r.db (mempoolCF r.db) (TxKey h)++-- | Mempool inputs that spend the outputs of a transaction, by output index.+-- The transaction may be confirmed or in the mempool.+getMempoolSpenders :: DatabaseReader -> TxHash -> IO (IntMap Spender)+getMempoolSpenders r h = do+  rows <- matchingAsListCF r.db (mempoolCF r.db) (MemSpenderKeyS h)+  return $ IntMap.fromList [(fromIntegral p.index, s) | (MemSpenderKey p, s) <- rows]++-- | Delete every key of the mempool area, and say how many there were. The+-- chain is not touched: afterwards the database is that of a store whose+-- mempool is empty, as if every unconfirmed transaction had been deleted one+-- by one, except that it does not remember having seen them.+--+-- This is all that wiping the mempool takes in the layered layout, and what+-- can be done if the mempool area is ever found to be wrong.+wipeMempoolArea :: DatabaseReader -> IO Int+wipeMempoolArea r = sum <$> mapM wipe [mempoolCF r.db, mempoolAddrCF r.db]+  where+    wipe cf = go 0+      where+        go n =+          firstKeys cf >>= \case+            [] -> return n+            ks -> writeBatch r.db (map (DelCF cf) ks) >> go (n + length ks)+    firstKeys :: ColumnFamily -> IO [ByteString]+    firstKeys cf = withIterCF r.db cf $ \it -> iterFirst it >> collect it (10000 :: Int)+    collect _ 0 = return []+    collect it n =+      iterKey it >>= \case+        Nothing -> return []+        Just k -> iterNext it >> (k :) <$> collect it (n - 1)++-------------------------------------------------------------------------------+-- Applying the mempool on top of the chain+-------------------------------------------------------------------------------++-- | Outputs of an address that the mempool created and has not spent,+-- newest first.+mempoolUnspents :: AddressMempool -> [Unspent]+mempoolUnspents m = [u | u <- m.created, not (HashMap.member u.outpoint m.spent)]++-- | Outputs of an address that the chain has as unspent and that the mempool+-- spends, with their amounts: those that the mempool spends and did not+-- create itself.+hiddenOutputs :: AddressMempool -> HashMap OutPoint Word64+hiddenOutputs m = HashMap.filterWithKey (\p _ -> not (HashSet.member p made)) m.spent+  where+    made = HashSet.fromList (map (.outpoint) m.created)++-- | The balance that the chain has for an address, with what the mempool+-- does at the address applied: the balance as the API gives it.+--+-- As in version 20, an output of a block that a mempool transaction spends no+-- longer counts as confirmed balance, and the unconfirmed balance is what+-- the mempool pays to the address and has not spent.+--+-- The amounts taken from the chain can never be more than it has, as long+-- as every output that the mempool spends exists. If they ever are, the+-- result stops at zero rather than wrapping around.+applyMempool :: Balance -> AddressMempool -> Balance+applyMempool c m = applySummary c (summarise m)++-- | Balance of an address as the API gives it, from its balance in the+-- chain, if it has one, and what the mempool does at it ('applyMempool').+-- 'Nothing' if the address was never used in either.+--+-- Version 20 answers with a balance of zero, rather than with 'Nothing',+-- for an address whose only transactions were deleted from the mempool: it+-- leaves the balance that it wrote for them behind. The API shows an unknown+-- address as a balance of zero, so the two are the same to it.+mergeBalance :: Address -> Maybe Balance -> AddressMempool -> Maybe Balance+mergeBalance a chain m+  | isNothing chain && null m.txs && HashMap.null m.spent && null m.created = Nothing+  | otherwise = Just (applyMempool (fromMaybe (zeroBalance a) chain) m)++-------------------------------------------------------------------------------+-- The summary of an address+-------------------------------------------------------------------------------++-- | What the entries of an address add up to.+summarise :: AddressMempool -> MemSummary+summarise m =+  MemSummary+    { txs = fromIntegral (length m.txs),+      paid = sum (map (.value) m.created),+      paidCount = fromIntegral (length m.created),+      spent = sum (HashMap.elems m.spent),+      spentCount = fromIntegral (HashMap.size m.spent),+      both = sum (map (.value) inside),+      bothCount = fromIntegral (length inside)+    }+  where+    inside = [u | u <- m.created, HashMap.member u.outpoint m.spent]++-- | The summaries that go with some entries by address, which may be of+-- many addresses: what the column family must have next to them, for the+-- migration, for repairs and for the tests.+summaryEntries :: [(MemAddrKey, MemAddrVal)] -> [(MemAddrKey, MemAddrVal)]+summaryEntries rows =+  [ (MemAddrSumKey a, MemAddrSumVal (summarise m))+  | (a, m) <- HashMap.toList (HashMap.fromListWith (flip merge) (mapMaybe single rows))+  ]+  where+    entries ts sp cr = AddressMempool {txs = ts, spent = sp, created = cr}+    merge x y = entries (x.txs <> y.txs) (x.spent <> y.spent) (x.created <> y.created)+    single = \case+      (MemAddrTxKey a t h, _) -> Just (a, entries [TxRef {block = MemRef t, txid = h}] HashMap.empty [])+      (MemAddrSpentKey a p, MemAddrSpentVal v) -> Just (a, entries [] (HashMap.singleton p v) [])+      (MemAddrOutKey a t p, MemAddrOutVal v s) ->+        let u = Unspent {block = MemRef t, outpoint = p, value = v, script = s, address = Just a}+         in Just (a, entries [] HashMap.empty [u])+      _ -> Nothing++-- | The balance that the chain has for an address with the summary of what+-- the mempool does at it applied: what 'applyMempool' gives, from the+-- summary instead of the entries.+applySummary :: Balance -> MemSummary -> Balance+applySummary c s =+  Balance+    { address = c.address,+      confirmed = c.confirmed `minus` (s.spent `minus` s.both),+      unconfirmed = c.unconfirmed + (s.paid `minus` s.both),+      utxo = (c.utxo `minus` (s.spentCount `minus` s.bothCount)) + (s.paidCount `minus` s.bothCount),+      txs = c.txs + s.txs,+      received = c.received + s.paid+    }+  where+    minus x y = if y > x then 0 else x - y++-- | Balance of an address as the API gives it, from its balance in the+-- chain and its summary in the mempool, either of which it may not have:+-- what 'mergeBalance' gives, without reading the entries.+summaryBalance :: Address -> Maybe Balance -> Maybe MemSummary -> Maybe Balance+summaryBalance _ chain Nothing = chain+summaryBalance a chain (Just s) = Just (applySummary (fromMaybe (zeroBalance a) chain) s)++-- | The summaries of addresses, in their order, read by key with one+-- multi-get: 'Nothing' for an address that the mempool does not touch,+-- which is nearly all of them, and which the filter of the column family+-- answers.+getSummaries :: DatabaseReader -> [Address] -> IO [Maybe MemSummary]+getSummaries r as =+  map (>>= decoded) <$> case map (encode . MemAddrSumKey) as of+    [] -> return []+    -- one key is cheaper to read by itself than in a multi-get+    [key] -> pure <$> getCF r.db (mempoolAddrCF r.db) key+    keys -> multiGetCF r.db (mempoolAddrCF r.db) keys+  where+    decoded bytes = case decode bytes of+      Right (MemAddrSumVal s) -> Just s+      _ -> Nothing++-- | The addresses whose summary is not what their entries add up to, each+-- with the summary that it should have and the one that it has. There are+-- none unless something is broken: this is what the tests hold the rules+-- to after every operation.+wrongSummaries :: DatabaseReader -> IO [(MemAddrKey, Maybe MemAddrVal, Maybe MemAddrVal)]+wrongSummaries r = do+  rows <- matchingAsListCF r.db (mempoolAddrCF r.db) MemAddrKeyS+  let found = HashMap.fromList [(k, v) | (k@MemAddrSumKey {}, v) <- rows]+      expected = HashMap.fromList (summaryEntries rows)+  return+    [ (k, HashMap.lookup k expected, HashMap.lookup k found)+    | k <- HashMap.keys (HashMap.union expected found),+      HashMap.lookup k expected /= HashMap.lookup k found+    ]++-- | Everything that the column family of entries by address holds for a+-- mempool of some transactions, serialized, in no order: the entries of+-- each, and the summaries of the addresses that they touch.+addressArea :: [MemRecords] -> [(ByteString, ByteString)]+addressArea records = [(encode k, encode v) | (k, v) <- entries <> summaryEntries entries]+  where+    entries = concatMap (.addresses) records++-- | Work every summary out again from the entries, and say how many+-- addresses have one. The migration does this once everything is moved, and+-- it is what puts the summaries right if they are ever in doubt.+rebuildSummaries :: DatabaseReader -> IO Int+rebuildSummaries r = do+  rows <- matchingAsListCF r.db cf MemAddrKeyS+  let old = [DelCF cf (encode k) | (k@MemAddrSumKey {}, _ :: MemAddrVal) <- rows]+      new = summaryEntries rows+  writeBatch r.db (old <> [PutCF cf (encode k) (encode v) | (k, v) <- new])+  return (length new)+  where+    cf = mempoolAddrCF r.db
+ src/Haskoin/Store/Database/Migrate.hs view
@@ -0,0 +1,678 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE MultiWayIf #-}++-- |+-- Module      : Haskoin.Store.Database.Migrate+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Migration of a database of version 20 to the layered layout, version 21,+-- of @docs\/layered-database.md@: the column families of the chain are left+-- with nothing from the mempool, and the mempool gets an area of its own+-- ("Haskoin.Store.Database.Mempool").+--+-- The store runs it when it opens a database ('upgradeDatabase'). It is+-- what makes the change of layout possible without syncing the chain again.+--+-- = What it does+--+-- In version 20 a mempool transaction is written into the indices of the+-- chain: its body, the entries of its addresses, its outputs as unspent, the+-- outputs it spends as spent, with itself as their spender in the bodies of+-- the transactions that created them, and all of it in the balances. The+-- migration takes each mempool transaction out of those indices, exactly as+-- deleting it does in version 20, and writes it to the mempool area instead.+-- When it is done, the chain's column families hold what they would hold if+-- the mempool had been wiped, and the mempool area holds the mempool.+--+-- It reads and writes only what the mempool touches, so it takes a time in+-- proportion to the mempool, not to the chain: about what wiping the+-- mempool takes in version 20.+--+-- = Why its own rules+--+-- Taking a transaction out of the indices of version 20 is what+-- "Haskoin.Store.Logic" does today when it deletes one. The migration does+-- not call that code: the rules of the store will change with the layout,+-- and a migration has to go on reading version 20 as it was. It carries the+-- few rules it needs, and the tests check them against "Haskoin.Store.Logic"+-- while both exist.+--+-- = Safe to interrupt+--+-- A mempool can hold hundreds of thousands of transactions, too many for one+-- write, so the migration works in batches. Each batch is one atomic write+-- that moves whole transactions, descendants before the transactions they+-- spend. After any batch the database is a valid mix: the transactions+-- already moved are in the mempool area, and the others are still in the+-- chain's indices, as a database of version 20 with a smaller mempool. The+-- migration can stop there and carry on later.+--+-- The first write sets the new version and a marker, t'MigrationKey', so+-- that a store of version 20 refuses the database from then on and a store+-- that knows the new layout carries the migration on. The last write+-- removes the marker. Before the first write everything is read once, so+-- that a database that the migration cannot handle is refused untouched.+--+-- = Version 22: records of transactions that are written once+--+-- Version 21 still had two things in the record of a confirmed+-- transaction that were not settled by the block that confirmed it:+--+-- * __its time__, which was the time at which the store first saw it in+--   its mempool, if it had, and so another on each store;+--+-- * __the spenders of its outputs__, which were written into the record+--   as later blocks spent them, and taken out when such a block was+--   reverted: the same on every store, but a record that was read and+--   written whole again for each of its outputs, for as long as it had+--   one unspent.+--+-- From version 22 a confirmed transaction has the time of its block, the+-- spenders of its outputs are under keys of their own+-- as t'Haskoin.Store.Database.Types.SpenderKey' says, and its record is written+-- when a block confirms it and deleted if that block is reverted, and+-- nothing else ('Haskoin.Store.Logic.Layered.importBlock').+-- 'migrateRecords' brings the records that are there already to that: it+-- gives each the time of its block, and moves its spenders to their keys.+-- It reads every transaction, and writes nearly every one, since most+-- have an output that was spent, so it takes a time in proportion to the+-- chain: 'upgradeDatabase' says how far it is as it goes. Each write does+-- whole transactions, the keys with the record that they come out of, and+-- running it again skips what is done, so it can be interrupted and+-- started over.+module Haskoin.Store.Database.Migrate+  ( upgradeDatabase,+    currentVersion,+    migrateRecords,+    layeredVersion,+    Migration (..),+    MigrationError (..),+    MigrationOptions (..),+    defaultMigrationOptions,+    migrateLayered,+    migrateLayeredWith,+  )+where++import Control.Monad (forM_, unless, void, when)+import Data.Default (def)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict qualified as IntMap+import Data.Maybe (isJust, mapMaybe)+import Data.Serialize (decode, encode)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Word (Word32, Word64)+import Database.RocksDB+  ( BatchOp,+    IterConfig (..),+    getCF,+    iterEntry,+    iterFirst,+    iterGetError,+    iterNext,+    withIterConfig,+  )+import Database.RocksDB.Query+  ( deleteOp,+    deleteOpCF,+    insertOp,+    insertOpCF,+    matchingAsListCF,+    retrieve,+    retrieveCF,+    writeBatch,+  )+import Haskoin+  ( Address,+    BlockHeader (..),+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    eitherToMaybe,+    nullOutPoint,+    scriptToAddressBS,+    txHash,+    txHashToHex,+  )+import Haskoin.Store.Common (nub')+import Haskoin.Store.Data+  ( BlockData (..),+    BlockRef (..),+    Prev (..),+    Spender (..),+    TxData (..),+    TxRef (..),+    UnixTime,+    confirmed,+  )+import Haskoin.Store.Database.Mempool (insertRecordOps, mempoolRecords, rebuildSummaries)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    addrOutCF,+    addrTxCF,+    balanceCF,+    initRocksDB,+    mempoolCF,+    spenderCF,+    txCF,+    unspentCF,+  )+import Haskoin.Store.Database.Types+  ( AddrOutKey (..),+    AddrTxKey (..),+    BalKey (..),+    BalVal (..),+    MemTxKey (..),+    MigrationKey (..),+    OutVal (..),+    SpenderKey (..),+    SpenderVal (..),+    TxKey (..),+    UnspentKey (..),+    UnspentVal (..),+    VersionKey (..),+    spenderGroup,+  )+import UnliftIO+  ( Exception,+    IORef,+    modifyIORef',+    newIORef,+    readIORef,+    throwIO,+    writeIORef,+  )++-- | Version of the layered layout as it was first written, with the time+-- at which a store first saw a transaction kept in the chain.+layeredVersion :: Word32+layeredVersion = 21++-- | Version that the store runs on: the layered layout, with the time of+-- its block for every confirmed transaction.+currentVersion :: Word32+currentVersion = 22++-- | Version that the migration starts from.+mixedVersion :: Word32+mixedVersion = 20++-- | What a migration did.+data Migration = Migration+  { -- | mempool transactions moved to the mempool area by this run+    moved :: !Int,+    -- | writes to the database that moved them+    batches :: !Int,+    -- | mempool transactions checked at the end, moved by this run or by+    -- an earlier one that was interrupted+    checked :: !Int,+    -- | records of confirmed transactions that this run wrote again, with+    -- the time of their block and without the spenders of their outputs+    rewritten :: !Int+  }+  deriving (Eq, Show)++-- | Why a migration refused a database, or stopped.+data MigrationError+  = -- | the database is of a version that this migration does not start+    -- from, or has none+    UnknownVersion !(Maybe Word32)+  | -- | the database is not as its version says it should be; nothing was+    -- written for the transaction that showed it+    Inconsistent !Text+  deriving (Eq, Show, Exception)++-- | How to run a migration.+data MigrationOptions = MigrationOptions+  { -- | mempool transactions to move in each write+    chunk :: !Int,+    -- | called after each write with the number of transactions moved so+    -- far; the tests interrupt the migration from here+    afterBatch :: Int -> IO (),+    -- | where to say what is going on+    report :: Text -> IO ()+  }++-- | A thousand transactions in each write, the size of the batches that+-- wipe the mempool in version 20, and nothing said.+defaultMigrationOptions :: MigrationOptions+defaultMigrationOptions =+  MigrationOptions+    { chunk = 1000,+      afterBatch = \_ -> return (),+      report = \_ -> return ()+    }++-- | Bring a database opened with+-- 'Haskoin.Store.Database.Reader.withLayeredReader' to the layout that the+-- store runs on, whatever it was, telling a function what is being done:+-- a new database gets the current version, one of version 18 or 19 is+-- first brought to version 20 as it always was, one of version 20 is+-- migrated to the layered layout ('migrateLayered'), and one of version 21+-- gets the time of its block for every confirmed transaction+-- ('migrateRecords'), each from the start or after an earlier run that was+-- interrupted. Throws a 'MigrationError' for any other version.+--+-- This is the point of no return for a database of an older version: a+-- store older than the layout refuses it afterwards.+upgradeDatabase :: (Text -> IO ()) -> DatabaseReader -> IO Migration+upgradeDatabase report r = do+  version <- retrieve r.db VersionKey+  marker <- retrieve r.db MigrationKey :: IO (Maybe Word32)+  case version of+    Nothing -> do+      writeBatch r.db [insertOp VersionKey currentVersion]+      return nothing+    Just v+      | v == 18 || v == 19 -> do+          report $ "Upgrading the database from version " <> cs (show v) <> " to " <> cs (show mixedVersion)+          initRocksDB r+          layered >>= times+      | v == currentVersion && marker == Nothing -> return nothing+      -- the times were being set when the store last stopped+      | v == currentVersion && marker == Just layeredVersion -> times nothing+      | v == currentVersion -> throwIO (UnknownVersion version)+    Just (_ :: Word32) -> layered >>= times+  where+    nothing = Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}+    layered = migrateLayeredWith defaultMigrationOptions {report = report} r+    times done = do+      rewritten <- migrateRecords report r+      return (done :: Migration) {rewritten}++-- | Bring the records of the confirmed transactions of a database of+-- version 21 to what version 22 writes, and make the database one of that+-- version; or carry that on in a database where it was interrupted. Each+-- record gets the time of its block, and the spenders of its outputs go+-- from the record to keys of their own. Says how many records it wrote.+-- Throws a 'MigrationError' for a database of another version, and for+-- one with a confirmed transaction at a height where the main chain has+-- no block.+--+-- Every record is read, and nearly every one is written: the whole of the+-- largest column family, which on a database of Bitcoin is many hours,+-- and room on the disk for a second copy of it until RocksDB has+-- compacted the first away. A function is told how far it is, every+-- million transactions.+--+-- The first write sets the new version and the marker of a migration under+-- way; the last removes the marker. In between each write does some+-- thousand transactions, each with its keys, and a database that is half+-- done is a valid mix of both kinds of record. Starting again reads from+-- the beginning and skips what is done.+migrateRecords :: (Text -> IO ()) -> DatabaseReader -> IO Int+migrateRecords report r = do+  version <- retrieve db VersionKey+  marker <- retrieve db MigrationKey :: IO (Maybe Word32)+  if+    | version == Just layeredVersion && marker == Nothing -> do+        report "Making the record of every confirmed transaction one that is written once: this reads them all"+        writeBatch db [insertOp VersionKey currentVersion, insertOp MigrationKey layeredVersion]+        run+    | version == Just currentVersion && marker == Just layeredVersion -> do+        report "Carrying on with the records of confirmed transactions, from the first"+        run+    | version == Just currentVersion && marker == Nothing -> return 0+    | otherwise -> throwIO (UnknownVersion version)+  where+    db = r.db+    run = do+      times <- newIORef IntMap.empty+      pending <- newIORef []+      scanned <- newIORef (0 :: Int)+      changed <- newIORef (0 :: Int)+      let write = do+            ops <- readIORef pending+            unless (null ops) (writeBatch db ops)+            writeIORef pending []+          -- the time of the block of the main chain at a height+          timeAt :: Word32 -> IO Word64+          timeAt height =+            IntMap.lookup (fromIntegral height) <$> readIORef times >>= \case+              Just t -> return t+              Nothing -> do+                blocks <- Q.getBlocksAtHeight r height >>= mapM (Q.getBlock r)+                case [b | Just b <- blocks, b.main] of+                  [b] -> do+                    let t = fromIntegral b.header.timestamp+                    t <$ modifyIORef' times (IntMap.insert (fromIntegral height) t)+                  _ -> throwIO (Inconsistent ("No block of the main chain at height " <> cs (show height)))+          one (key, value) = case (decode key, decode value) of+            (Right (TxKey h), Right (td :: TxData)) -> do+              case td.block of+                BlockRef {height} | not td.deleted -> do+                  t <- timeAt height+                  -- the spenders of its outputs get keys of their own,+                  -- in the write that takes them out of the record+                  let grouped =+                        IntMap.fromListWith+                          IntMap.union+                          [ (i `div` fromIntegral spenderGroup, IntMap.singleton i s)+                          | (i, s) <- IntMap.toList td.spenders+                          ]+                      keys =+                        [ insertOpCF (spenderCF db) (SpenderKey h (fromIntegral g)) (SpenderVal m)+                        | (g, m) <- IntMap.toList grouped+                        ]+                      -- a confirmed transaction cannot be replaced, and the+                      -- rules write it so: one that says otherwise is put right+                      record = (td :: TxData) {timestamp = t, spenders = IntMap.empty, rbf = False}+                  when (td.timestamp /= t || td.rbf || not (null keys)) $ do+                    modifyIORef' pending ((insertOpCF (txCF db) (TxKey h) record : keys) <>)+                    modifyIORef' changed (+ 1)+                    n <- length <$> readIORef pending+                    when (n >= 4000) write+                _ -> return ()+              n <- readIORef scanned+              writeIORef scanned (n + 1)+              when ((n + 1) `mod` 1000000 == 0) $ do+                c <- readIORef changed+                report $ cs (show (n + 1)) <> " transactions read, " <> cs (show c) <> " written again"+            -- the short keys of the column family are not transactions+            _ -> return ()+          go it =+            iterEntry it >>= \case+              Nothing -> iterGetError it >>= mapM_ (throwIO . Inconsistent . cs . show)+              Just row -> one row >> iterNext it >> go it+      -- every transaction, whatever its prefix: the column family has a+      -- prefix for its filters, and an iterator there promises the keys of+      -- one prefix only unless it is asked for the order of them all+      withIterConfig db (Just (txCF db)) def {totalOrderSeek = True} (\it -> iterFirst it >> go it)+      write+      writeBatch db [deleteOp MigrationKey]+      c <- readIORef changed+      n <- readIORef scanned+      report $ cs (show c) <> " of " <> cs (show n) <> " records of transactions written again"+      return c++-- | Migrate a database of version 20, opened with+-- 'Haskoin.Store.Database.Reader.withLayeredReader', to the layered layout,+-- or carry on a migration that was interrupted. Does nothing to a database+-- that is already there. Throws a 'MigrationError' otherwise.+migrateLayered :: DatabaseReader -> IO Migration+migrateLayered = migrateLayeredWith defaultMigrationOptions++-- | 'migrateLayered' with options.+migrateLayeredWith :: MigrationOptions -> DatabaseReader -> IO Migration+migrateLayeredWith opts r = do+  version <- retrieve db VersionKey+  marker <- retrieve db MigrationKey :: IO (Maybe Word32)+  if+    -- a database that is past this migration, whatever came after it+    | version == Just layeredVersion && marker == Nothing ->+        return Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}+    | version == Just currentVersion ->+        return Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}+    | version == Just layeredVersion && marker == Just mixedVersion -> do+        opts.report "Carrying on with the migration to the layered layout"+        run+    | version == Just mixedVersion -> do+        listed <- mempoolList+        mapM_ (preflight r) listed+        opts.report $ "Moving " <> cs (show (length listed)) <> " mempool transactions to their own area"+        -- from here on a store of version 20 refuses the database+        writeBatch db [insertOp VersionKey layeredVersion, insertOp MigrationKey mixedVersion]+        run+    | otherwise -> throwIO (UnknownVersion version)+  where+    db = r.db+    mempoolList = do+      rows <- matchingAsListCF db (mempoolCF db) MemTxKeyS+      return [h | (MemTxKey h, _ :: UnixTime) <- rows]+    run = do+      env <- newEnv opts r+      listed <- mempoolList+      mapM_ (moveTx env) listed+      flush env+      mapM_ (verify r) listed+      -- the summaries are worked out from the entries, all at once: doing it+      -- again after an interruption gives the same+      void (rebuildSummaries r)+      writeBatch db [deleteOp MigrationKey]+      moved <- readIORef env.total+      batches <- readIORef env.written+      opts.report $ "Moved " <> cs (show moved) <> " mempool transactions"+      return Migration {moved, batches, checked = length listed, rewritten = 0}++-------------------------------------------------------------------------------+-- A batch+-------------------------------------------------------------------------------++-- | A migration under way, and the batch that it is building: what it will+-- write, with the last value of everything that is read again before the+-- write.+data Env = Env+  { reader :: !DatabaseReader,+    options :: !MigrationOptions,+    -- | bodies in the chain's column family that the batch changes;+    -- 'Nothing' for one that it removes+    bodies :: !(IORef (HashMap TxHash (Maybe TxData))),+    -- | balances in the chain's column family that the batch changes+    balances :: !(IORef (HashMap Address BalVal)),+    -- | everything else that the batch writes, newest first: each of these+    -- keys is written without being read, and in this order+    ops :: !(IORef [BatchOp]),+    -- | transactions that the batch moves+    moving :: !(IORef (HashSet TxHash)),+    -- | transactions moved by this run+    total :: !(IORef Int),+    -- | batches written by this run+    written :: !(IORef Int)+  }++-- | A migration that has moved nothing yet, with an empty batch.+newEnv :: MigrationOptions -> DatabaseReader -> IO Env+newEnv options reader = do+  bodies <- newIORef HashMap.empty+  balances <- newIORef HashMap.empty+  ops <- newIORef []+  moving <- newIORef HashSet.empty+  total <- newIORef 0+  written <- newIORef 0+  return Env {reader, options, bodies, balances, ops, moving, total, written}++-- | Body of a transaction in the chain's column family, as the batch leaves+-- it.+chainBody :: Env -> TxHash -> IO (Maybe TxData)+chainBody env h =+  HashMap.lookup h <$> readIORef env.bodies >>= \case+    Just changed -> return changed+    Nothing -> retrieveCF db (txCF db) (TxKey h)+  where+    db = env.reader.db++-- | Change the balance of an address in the chain's column family, or stop+-- if the change cannot be made.+change :: Env -> Address -> (BalVal -> Either Text BalVal) -> IO ()+change env a f = do+  current <-+    HashMap.lookup a <$> readIORef env.balances >>= \case+      Just b -> return b+      Nothing -> maybe def id <$> retrieveCF db (balanceCF db) (BalKey a)+  new <- either (throwIO . Inconsistent) return (f current)+  modifyIORef' env.balances (HashMap.insert a new)+  where+    db = env.reader.db++-- | Add an operation to the batch.+op :: Env -> BatchOp -> IO ()+op env o = modifyIORef' env.ops (o :)++-- | Write the batch, and start another.+flush :: Env -> IO ()+flush env = do+  count <- HashSet.size <$> readIORef env.moving+  unless (count == 0) $ do+    others <- reverse <$> readIORef env.ops+    bodies <- HashMap.toList <$> readIORef env.bodies+    balances <- HashMap.toList <$> readIORef env.balances+    writeBatch db $+      others+        <> [maybe (deleteOpCF (txCF db) (TxKey h)) (insertOpCF (txCF db) (TxKey h)) b | (h, b) <- bodies]+        <> [balanceOp a b | (a, b) <- balances]+    writeIORef env.ops []+    writeIORef env.bodies HashMap.empty+    writeIORef env.balances HashMap.empty+    writeIORef env.moving HashSet.empty+    modifyIORef' env.written (+ 1)+    readIORef env.total >>= env.options.afterBatch+  where+    db = env.reader.db+    -- an address that only the mempool used has no balance in the chain+    balanceOp a b+      | b == def = deleteOpCF (balanceCF db) (BalKey a)+      | otherwise = insertOpCF (balanceCF db) (BalKey a) b++-------------------------------------------------------------------------------+-- Moving a transaction+-------------------------------------------------------------------------------++-- | Whether a transaction is already in the mempool area.+isMoved :: Env -> TxHash -> IO Bool+isMoved env h = do+  now <- HashSet.member h <$> readIORef env.moving+  if now then return True else isJust <$> getCF db (mempoolCF db) (encode (TxKey h))+  where+    db = env.reader.db++-- | Move a mempool transaction, and first the mempool transactions that+-- spend its outputs: taking those out is what makes its outputs unspent+-- again, as they must be for its own turn.+moveTx :: Env -> TxHash -> IO ()+moveTx env h = do+  done <- isMoved env h+  unless done $ do+    td <- mempoolBody+    forM_ (nub' (map (.txid) (IntMap.elems td.spenders))) (moveTx env)+    -- read again: moving the spenders removed them from the body+    td' <- mempoolBody+    unless (IntMap.null td'.spenders) . stop $ "still has spenders after they were moved"+    time <- case td'.block of+      MemRef t -> return t+      BlockRef {} -> stop "is confirmed"+    takeOut env td'+    mapM_ (op env) (insertRecordOps db (mempoolRecords env.reader.net env.reader.ctx time td'))+    modifyIORef' env.moving (HashSet.insert h)+    modifyIORef' env.total (+ 1)+    count <- HashSet.size <$> readIORef env.moving+    when (count >= env.options.chunk) (flush env)+  where+    db = env.reader.db+    stop :: Text -> IO a+    stop why = throwIO . Inconsistent $ "Mempool transaction " <> txHashToHex h <> " " <> why+    mempoolBody =+      chainBody env h >>= \case+        Just td | not td.deleted, not (confirmed td.block) -> return td+        Just td | td.deleted -> stop "is marked as deleted"+        Just _ -> stop "is confirmed"+        Nothing -> stop "has no body"++-- | Take a mempool transaction that nothing spends out of the indices of+-- the chain, as deleting it does in version 20, except that its body is+-- removed and not kept as deleted: it is not deleted, it moves.+takeOut :: Env -> TxData -> IO ()+takeOut env td = do+  -- the entries of its addresses, and their counts of transactions+  forM_ touched $ \a -> do+    op env (deleteOpCF (addrTxCF db) (AddrTxKey a TxRef {block = td.block, txid = h}))+    change env a $ \b -> do+      n <- less "transactions" b.txs 1+      return (b :: BalVal) {txs = n}+  -- its outputs, all of them unspent by now+  forM_ (zip [0 ..] td.tx.outputs) $ \(i, o) -> do+    let p = OutPoint h i+    op env (deleteOpCF (unspentCF db) (UnspentKey p))+    forM_ (address o.script) $ \a -> do+      op env (deleteOpCF (addrOutCF db) (AddrOutKey a td.block p))+      change env a $ \b -> do+        u <- less "unconfirmed balance" b.unconfirmed o.value+        n <- less "unspent outputs" b.utxo 1+        rx <- less "amount received" b.received o.value+        return (b :: BalVal) {unconfirmed = u, utxo = n, received = rx}+  -- the outputs it spends are unspent again, as before it came+  forM_ [i.outpoint | i <- td.tx.inputs, i.outpoint /= nullOutPoint] $ \p -> do+    parent <-+      chainBody env p.hash >>= \case+        Just t | not t.deleted -> return t+        _ -> stop $ "spends an output of " <> txHashToHex p.hash <> ", which is not there"+    o <- case drop (fromIntegral p.index) parent.tx.outputs of+      o : _ -> return o+      [] -> stop $ "spends an output that " <> txHashToHex p.hash <> " does not have"+    modifyIORef' env.bodies . HashMap.insert p.hash . Just $+      (parent :: TxData) {spenders = IntMap.delete (fromIntegral p.index) parent.spenders}+    op env . insertOpCF (unspentCF db) (UnspentKey p) $+      UnspentVal {block = parent.block, value = o.value, script = o.script}+    forM_ (address o.script) $ \a -> do+      op env (insertOpCF (addrOutCF db) (AddrOutKey a parent.block p) (OutVal o.value o.script))+      change env a $ \b ->+        return $+          if confirmed parent.block+            then (b :: BalVal) {confirmed = b.confirmed + o.value, utxo = b.utxo + 1}+            else (b :: BalVal) {unconfirmed = b.unconfirmed + o.value, utxo = b.utxo + 1}+  -- its body leaves the chain's column family+  modifyIORef' env.bodies (HashMap.insert h Nothing)+  where+    db = env.reader.db+    h = txHash td.tx+    address = eitherToMaybe . scriptToAddressBS env.reader.net env.reader.ctx+    touched =+      nub' $+        mapMaybe (address . (.script)) (IntMap.elems td.prevs)+          <> mapMaybe (address . (.script)) td.tx.outputs+    stop :: Text -> IO a+    stop why = throwIO . Inconsistent $ "Mempool transaction " <> txHashToHex h <> " " <> why+    -- a balance never goes below zero in a database that is in order+    less :: Text -> Word64 -> Word64 -> Either Text Word64+    less what x y+      | y > x = Left $ "Taking out " <> txHashToHex h <> " would make the " <> what <> " of an address negative"+      | otherwise = Right (x - y)++-------------------------------------------------------------------------------+-- Checks+-------------------------------------------------------------------------------++-- | Before anything is written: a transaction that the mempool lists must+-- have the body of a mempool transaction.+preflight :: DatabaseReader -> TxHash -> IO ()+preflight r h =+  (retrieveCF r.db (txCF r.db) (TxKey h) :: IO (Maybe TxData)) >>= \case+    Just td | not td.deleted, not (confirmed td.block) -> return ()+    _ ->+      throwIO . Inconsistent $+        "The mempool lists " <> txHashToHex h <> ", which is not an unconfirmed transaction"++-- | After everything is moved: a mempool transaction is in the mempool+-- area and has left nothing in the chain's column families, and every+-- output it spends is there to be spent, in the mempool or unspent in the+-- chain.+verify :: DatabaseReader -> TxHash -> IO ()+verify r h = do+  td <-+    retrieveCF db (mempoolCF db) (TxKey h) >>= \case+      Just td -> return (td :: TxData)+      Nothing -> stop "is not in the mempool area"+  inChain <- isJust <$> getCF db (txCF db) (encode (TxKey h))+  when inChain $ stop "still has a body in the chain"+  forM_ (zip [0 ..] td.tx.outputs) $ \(i, _) -> do+    left <- isJust <$> getCF db (unspentCF db) (encode (UnspentKey (OutPoint h i)))+    when left $ stop "still has an unspent output in the chain"+  forM_ [i.outpoint | i <- td.tx.inputs, i.outpoint /= nullOutPoint] $ \p -> do+    inMempool <- isJust <$> getCF db (mempoolCF db) (encode (TxKey p.hash))+    unspent <- isJust <$> getCF db (unspentCF db) (encode (UnspentKey p))+    unless (inMempool || unspent) . void . stop $+      "spends an output of " <> txHashToHex p.hash <> " that is neither in the mempool nor unspent"+  where+    db = r.db+    stop :: Text -> IO a+    stop why = throwIO . Inconsistent $ "After the migration, mempool transaction " <> txHashToHex h <> " " <> why
+ src/Haskoin/Store/Database/Query.hs view
@@ -0,0 +1,696 @@+-- |+-- Module      : Haskoin.Store.Database.Query+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Queries against the RocksDB database, as plain functions in 'IO' that take+-- the 'DatabaseReader' as their first argument.+--+-- These read one layer. On a database of version 20 that is everything,+-- the mempool included, and the tests use them so. On the layout that the+-- store runs on, they give what the chain has and nothing of the mempool,+-- but for its list: "Haskoin.Store.Database.Query.Layered" calls them for+-- the chain's part of its answers, and is what the store answers with.+--+-- = Why plain functions+--+-- The queries used to be the methods of two classes, run in+-- @ReaderT DatabaseReader@, and reached the web server through more layers of+-- @ReaderT@ for the cache. Every caller then+-- had to live in a stack of monad transformers to ask the database anything,+-- and every layer had to lift or re-implement every method. A function that+-- takes the reader and runs in 'IO' can be called from anywhere, needs no+-- instance, and says in its type exactly what it reads from.+--+-- Streams are still conduits, run in 'IO': they are a good fit for merging the+-- sorted indices of many addresses and reading only as far as a page needs.+module Haskoin.Store.Database.Query+  ( -- * All of them+    databaseRead,++    -- * Blocks+    getBestBlock,+    getBlocksAtHeight,+    getBlock,++    -- * Transactions+    getTxData,+    getTxRecord,+    getManyTxData,+    getChainSpender,+    getChainSpenders,+    withChainSpenders,+    getNumTxData,+    getSpender,++    -- * Addresses+    getBalance,+    getBalances,+    getUnspent,+    getAddressTxs,+    getAddressesTxs,+    getAddressUnspents,+    getAddressesUnspents,++    -- * Mempool+    getMempool,+    getMempoolPage,+    getMempoolBefore,++    -- * Extended public keys+    xPubBals,+    xPubUnspents,+    xPubTxs,+    xPubTxCount,+    getAddressesTxCount,++    -- * Building blocks++    -- | What the queries above are made of, for the queries of the layered+    -- layout in "Haskoin.Store.Database.Query.Layered", which read the same+    -- column families for the part of their answers that is in the chain.+    count,+    withManyIters,+    retrieveManyCF,+    joinConduits,+    usedAddresses,+  )+where++import Conduit+  ( ConduitT,+    concatC,+    concatMapC,+    dropWhileC,+    lift,+    mapC,+    runConduit,+    sinkList,+    takeC,+    takeWhileC,+    yield,+    (.|),+  )+import Control.Monad (forM, (>=>))+import Data.Bits ((.&.))+import Data.ByteString qualified as BS+import Data.Default (def)+import Data.Functor ((<&>))+import Data.IntMap.Strict qualified as IntMap+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, listToMaybe)+import Data.Ord (Down (..))+import Data.Serialize (Serialize, decode, encode)+import Data.Word (Word32, Word64)+import Database.RocksDB+  ( ColumnFamily,+    DB,+    IterConfig (..),+    Iterator,+    createIteratorConfig,+    getCF,+    iterKey,+    iterLast,+    iterNext,+    iterPrev,+    iterSeek,+    iterValid,+    multiGetCF,+    prefixUpperBound,+    withIterCF,+    withIterConfig,+  )+import Database.RocksDB.Query+  ( matching,+    matchingAsListCF,+    matchingSkip,+    retrieve,+    retrieveCF,+  )+import Haskoin+  ( Address,+    BlockHash,+    BlockHeight,+    OutPoint (..),+    Tx (..),+    TxHash,+  )+import Haskoin.Store.Common+  ( Limits (..),+    Start (..),+    StoreRead (..),+    applyLimitsC,+    deOffset,+    joinDescStreams,+    mergeDescGroups,+    notDeleted,+    scanXPubBals,+    txRefKey,+    unspentKey,+  )+import Haskoin.Store.Data+import Haskoin.Store.Database.Reader+import Haskoin.Store.Database.Types+import System.Metrics.StatsD (decrementGauge, incrementCounter, incrementGauge)+import UnliftIO+  ( bracket,+    bracket_,+    mask_,+    modifyIORef',+    newIORef,+    readIORef,+  )++-- | The queries of a database as a 'StoreRead', for code that takes its+-- queries as a value, such as the web server, which puts the Redis cache in+-- front of some of them. Transactions that were deleted are left out+-- ('notDeleted'); the functions of this module still return them, for the+-- code that imports transactions.+databaseRead :: DatabaseReader -> StoreRead+databaseRead r =+  StoreRead+    { net = r.net,+      ctx = r.ctx,+      getBestBlock = getBestBlock r,+      getBlocksAtHeight = getBlocksAtHeight r,+      getBlock = getBlock r,+      getTxData = fmap notDeleted . getTxData r,+      getManyTxData = fmap (map notDeleted) . getManyTxData r,+      getSpender = getSpender r,+      getBalance = getBalance r,+      getUnspent = getUnspent r,+      getMempool = getMempool r,+      getMempoolPage = getMempoolPage r,+      getAddressesTxs = getAddressesTxs r,+      getAddressesUnspents = getAddressesUnspents r,+      initialGap = r.initGap,+      maxGap = r.maxGap,+      getNumTxData = fmap (filter (not . (.deleted))) . getNumTxData r,+      getBalances = getBalances r,+      getAddressTxs = getAddressTxs r,+      getAddressUnspents = getAddressUnspents r,+      xPubBals = xPubBals r,+      xPubUnspents = \_ -> xPubUnspents r,+      xPubTxs = \_ -> xPubTxs r,+      xPubTxCount = \_ -> xPubTxCount r,+      getAddressesTxCount = getAddressesTxCount r+    }++-- | Count something in the metrics, if they are enabled.+count :: DatabaseReader -> (DataMetrics -> IO ()) -> IO ()+count r f = mapM_ f r.metrics++-------------------------------------------------------------------------------+-- Blocks+-------------------------------------------------------------------------------++-- | Hash of the best block, if the database has any.+getBestBlock :: DatabaseReader -> IO (Maybe BlockHash)+getBestBlock r = do+  count r $ \s -> incrementCounter s.dataBestCount 1+  retrieve r.db BestKey++-- | Hashes of the blocks at a height, of the main chain and of side chains.+getBlocksAtHeight :: DatabaseReader -> BlockHeight -> IO [BlockHash]+getBlocksAtHeight r h =+  retrieveCF r.db (heightCF r.db) (HeightKey h) >>= \case+    Nothing -> return []+    Just ls -> do+      count r $ \s -> incrementCounter s.dataBlockCount (length ls)+      return ls++-- | Data of a block.+getBlock :: DatabaseReader -> BlockHash -> IO (Maybe BlockData)+getBlock r h =+  retrieveCF r.db (blockCF r.db) (BlockKey h) >>= \case+    Nothing -> return Nothing+    Just b -> do+      count r $ \s -> incrementCounter s.dataBlockCount 1+      return (Just b)++-------------------------------------------------------------------------------+-- Transactions+-------------------------------------------------------------------------------++-- | Data of a transaction, including deleted ones, with the inputs of+-- blocks that spend its outputs.+getTxData :: DatabaseReader -> TxHash -> IO (Maybe TxData)+getTxData r th =+  getTxRecord r th >>= \case+    Nothing -> return Nothing+    Just t -> do+      count r $ \s -> incrementCounter s.dataTxCount 1+      listToMaybe <$> withChainSpenders r [(th, t)]++-- | The record of a transaction as the database has it, including deleted+-- ones. Since version 22 the spenders of its outputs are not in it+-- ('getChainSpenders'); a database of version 20 or 21 has them there.+-- This is what the rules that write the database read: they must not+-- write back what was read from somewhere else.+getTxRecord :: DatabaseReader -> TxHash -> IO (Maybe TxData)+getTxRecord r th = retrieveCF r.db (txCF r.db) (TxKey th)++-- | Data of many transactions, in the order of the hashes, read with one+-- multi-get, with the inputs of blocks that spend their outputs.+getManyTxData :: DatabaseReader -> [TxHash] -> IO [Maybe TxData]+getManyTxData r ths = do+  found <- retrieveManyCF r.db (txCF r.db) (map TxKey ths)+  count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes found))+  spent <- withChainSpenders r [(h, td) | (h, Just td) <- zip ths found]+  return (fill found spent)+  where+    fill (Just _ : more) (td : tds) = Just td : fill more tds+    fill (Nothing : more) tds = Nothing : fill more tds+    fill _ _ = []++-- | The input of a block that spends an output, if one does: one read by+-- key, of the group of outputs that it is in, in a database of version 22.+getChainSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)+getChainSpender r p =+  retrieveCF r.db (spenderCF r.db) (spenderKey p) <&> \case+    Just (SpenderVal m) -> IntMap.lookup (fromIntegral p.index) m+    Nothing -> Nothing++-- | The inputs of blocks that spend the outputs of a transaction, by+-- output, read with one scan of the keys of the transaction.+getChainSpenders :: DatabaseReader -> TxHash -> IO (IntMap.IntMap Spender)+getChainSpenders r h = do+  rows <- matchingAsListCF r.db (spenderCF r.db) (SpenderKeyS h)+  return (IntMap.unions [m | (_ :: SpenderKey, SpenderVal m) <- rows])++-- | Records of transactions, with the inputs of blocks that spend their+-- outputs added to whatever spenders the records have themselves, which+-- those of a database older than version 22 do.+--+-- The spenders are read by the keys of the groups of outputs, all the+-- groups of all the transactions in one multi-get. A group is sixteen+-- outputs, so nearly every transaction is one key, and a page of a+-- hundred transactions is one more read of a hundred keys. A scan would+-- need an iterator for each transaction, at ten microseconds apiece. Only+-- a transaction with more groups than 'wideTx' is scanned, so that a+-- payout to thousands is not hundreds of keys to ask for.+--+-- Each record comes with the hash of its transaction, which whoever read+-- it by its key has: working it out again from the record is two rounds+-- of SHA-256 over the whole transaction, and cost more than everything+-- else here together when this was first written without it.+withChainSpenders :: DatabaseReader -> [(TxHash, TxData)] -> IO [TxData]+withChainSpenders _ [] = return []+withChainSpenders r tds = do+  let groups td = (fromIntegral (length td.tx.outputs) + spenderGroup - 1) `div` spenderGroup+      keys (h, td) = [SpenderKey h g | g <- take (fromIntegral (groups td)) [0 ..]]+      narrow (_, td) = groups td <= wideTx+      asked = concatMap keys (filter narrow tds)+  found <- case asked of+    [] -> return []+    _ -> retrieveManyCF r.db (spenderCF r.db) asked+  let byKey = Map.fromList [(k, m) | (k, Just (SpenderVal m)) <- zip asked found]+      keyed x = IntMap.unions [m | k <- keys x, Just m <- [Map.lookup k byKey]]+  forM tds $ \x@(h, td) -> do+    chain <- if narrow x then return (keyed x) else getChainSpenders r h+    return (td :: TxData) {spenders = IntMap.union td.spenders chain}++-- | The number of groups of outputs above which the spenders of a+-- transaction are read with a scan instead of by key.+wideTx :: Word32+wideTx = 16++-- | Transactions whose hash, read as a number, matches a short identifier of+-- the Blockchain.info API. There may be more than one.+getNumTxData :: DatabaseReader -> Word64 -> IO [TxData]+getNumTxData r i = do+  let (sk, w) = decodeTxKey i+  ls <- matchingAsListCF r.db (txCF r.db) (TxKeyS sk)+  let f h =+        let bs = encode h+            b = BS.head (BS.drop 6 bs)+            w' = b .&. 0xf8+         in w == w'+      txs = [(h, td) | (TxKey h, td) <- ls, f h]+  count r $ \s -> incrementCounter s.dataTxCount (length txs)+  withChainSpenders r txs++-- | Transaction that spends an output, if any: by its key, or, in a+-- database older than version 22, from the record of the transaction that+-- made the output.+getSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)+getSpender r op =+  getChainSpender r op >>= \case+    Just s -> return (Just s)+    Nothing ->+      getTxRecord r op.hash <&> \case+        Nothing -> Nothing+        Just td -> IntMap.lookup (fromIntegral op.index) td.spenders++-------------------------------------------------------------------------------+-- Addresses+-------------------------------------------------------------------------------++-- | Balance of an address, if it was ever used.+getBalance :: DatabaseReader -> Address -> IO (Maybe Balance)+getBalance r a = do+  count r $ \s -> incrementCounter s.dataBalanceCount 1+  fmap (valToBalance a) <$> retrieveCF r.db (balanceCF r.db) (BalKey a)++-- | Balances of addresses, in the same order, read with one multi-get.+-- Addresses never used get a zero balance.+getBalances :: DatabaseReader -> [Address] -> IO [Balance]+getBalances _ [] = return []+getBalances r as = do+  count r $ \s -> incrementCounter s.dataBalanceCount (length as)+  vals <- retrieveManyCF r.db (balanceCF r.db) (map BalKey as)+  return (zipWith f as vals)+  where+    f a Nothing = zeroBalance a+    f a (Just v) = valToBalance a v++-- | Unspent output, if it exists and is unspent.+getUnspent :: DatabaseReader -> OutPoint -> IO (Maybe Unspent)+getUnspent r p =+  retrieveCF r.db (unspentCF r.db) (UnspentKey p) >>= \case+    Nothing -> return Nothing+    Just val -> do+      count r $ \s -> incrementCounter s.dataUnspentCount 1+      return (Just (valToUnspent r.net r.ctx p val))++-- | Transactions of an address, newest first.+getAddressTxs :: DatabaseReader -> Address -> Limits -> IO [TxRef]+getAddressTxs r a limits = do+  txs <- withIterCFStat r (addrTxCF r.db) (AddrTxKeyA a) $ \it ->+    runConduit $+      addressConduit r a limits.start it+        .| applyLimitsC limits+        .| sinkList+  count r $ \s -> incrementCounter s.dataAddrTxCount (length txs)+  return txs++-- | Transactions of any of the addresses, newest first, each once.+getAddressesTxs :: DatabaseReader -> [Address] -> Limits -> IO [TxRef]+getAddressesTxs r addrs limits =+  withManyIters r (addrTxCF r.db) (map AddrTxKeyA addrs) $ \its -> do+    txs <- joinConduits txRefKey (zipWith (\a -> addressConduit r a limits.start) addrs its) limits+    count r $ \s -> incrementCounter s.dataAddrTxCount (length txs)+    return txs++-- | Unspent outputs of an address, newest first.+getAddressUnspents :: DatabaseReader -> Address -> Limits -> IO [Unspent]+getAddressUnspents r a limits = do+  us <- withIterCFStat r (addrOutCF r.db) (AddrOutKeyA a) $ \it ->+    runConduit $+      unspentConduit r a limits.start it+        .| applyLimitsC limits+        .| sinkList+  count r $ \s -> incrementCounter s.dataUnspentCount (length us)+  return us++-- | Unspent outputs of any of the addresses, newest first.+getAddressesUnspents :: DatabaseReader -> [Address] -> Limits -> IO [Unspent]+getAddressesUnspents r addrs limits =+  withManyIters r (addrOutCF r.db) (map AddrOutKeyA addrs) $ \its -> do+    uns <- joinConduits unspentKey (zipWith (\a -> unspentConduit r a limits.start) addrs its) limits+    count r $ \s -> incrementCounter s.dataUnspentCount (length uns)+    return uns++-- | Stream the transactions of an address, newest first, from a starting point.+-- If the start is a transaction that is not found, nothing is streamed.+addressConduit ::+  DatabaseReader ->+  Address ->+  Maybe Start ->+  Iterator ->+  ConduitT i TxRef IO ()+addressConduit r a s it =+  pipe .| mapC (uncurry f)+  where+    f (AddrTxKey _ t) () = t+    f _ _ = undefined+    pipe = case s of+      Nothing ->+        matching it (AddrTxKeyA a)+      Just (AtBlock bh) ->+        matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a (BlockRef bh maxBound))+      Just (AtTx txh) ->+        lift (getTxRecord r txh) >>= \case+          Just TxData {block = b@BlockRef {}} ->+            matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)+          Just TxData {block = b@MemRef {}} ->+            -- Mempool transactions that arrived in the same second are+            -- keyed by hash, so seek to the time and drop those before it.+            let start = txRefKey (TxRef b txh)+                before (AddrTxKey _ t, ()) = txRefKey t > start+                before _ = undefined+             in matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)+                  .| (dropWhileC before >> mapC id)+          Nothing -> return ()++-- | Stream the unspent outputs of an address, newest first, from a starting+-- point. If the start is a transaction that is not found, nothing is streamed.+unspentConduit ::+  DatabaseReader ->+  Address ->+  Maybe Start ->+  Iterator ->+  ConduitT i Unspent IO ()+unspentConduit r a s it =+  x .| mapC (uncurry (toUnspent r.net r.ctx)) .| concatC+  where+    x = case s of+      Nothing ->+        matching it (AddrOutKeyA a)+      Just (AtBlock h) ->+        matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a (BlockRef h maxBound))+      Just (AtTx txh) ->+        lift (getTxRecord r txh) >>= \case+          Just TxData {block = b@BlockRef {}} ->+            matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)+          Just TxData {block = b@MemRef {}} ->+            -- The outputs of the start transaction come first among those+            -- of the same second whose hashes are not smaller.+            let start = (b, Down (encode (OutPoint txh 0)))+                before (AddrOutKey _ ref p, _) = (ref, Down (encode p)) > start+                before _ = undefined+             in matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)+                  .| (dropWhileC before >> mapC id)+          Nothing -> return ()++-------------------------------------------------------------------------------+-- Mempool+-------------------------------------------------------------------------------++-- | The whole mempool, newest first.+getMempool :: DatabaseReader -> IO [(UnixTime, TxHash)]+getMempool r = getMempoolPage r def++-- | Page of the mempool, newest first, from the offset and limit of the+-- 'Limits'; its start is ignored. Reads only the entries of the page from+-- the index by time.+getMempoolPage :: DatabaseReader -> Limits -> IO [(UnixTime, TxHash)]+getMempoolPage r limits = do+  count r $ \s -> incrementCounter s.dataMempoolCount 1+  withIterCF r.db (mempoolCF r.db) $ \it ->+    runConduit $+      mempoolNewestC it+        .| applyLimitsC limits+        .| sinkList++-- | Mempool transactions that entered it before the given time, oldest+-- first, at most as many as given. Reads only those entries of the index.+getMempoolBefore :: DatabaseReader -> UnixTime -> Int -> IO [(UnixTime, TxHash)]+getMempoolBefore r time n =+  withIterCF r.db (mempoolCF r.db) $ \it ->+    runConduit $+      matching it MemTimeKeyS+        .| concatMapC entry+        .| takeWhileC ((< time) . fst)+        .| takeC n+        .| sinkList+  where+    entry (MemTimeKey t h, ()) = Just (t, h)+    entry (MemTimeKeyS, ()) = Nothing++-- | Mempool from the index by time, newest first. The index sorts last in+-- the mempool column family, so seek past its prefix and walk backwards+-- until a key is no longer in it.+mempoolNewestC :: Iterator -> ConduitT i (UnixTime, TxHash) IO ()+mempoolNewestC it = do+  lift $ do+    -- 0x0c is the first prefix after the 0x0b of 'MemTimeKey'+    iterSeek it (BS.singleton 0x0c)+    iterValid it >>= \case+      True -> iterPrev it+      False -> iterLast it+  go+  where+    go =+      lift (iterKey it) >>= \case+        Just k | Right (MemTimeKey t h) <- decode k -> do+          yield (t, h)+          lift (iterPrev it)+          go+        _ -> return ()++-------------------------------------------------------------------------------+-- Extended public keys+-------------------------------------------------------------------------------++-- | Balances of the addresses of an extended public key, derived up to the+-- exact gap (see 'scanXPubBals').+xPubBals :: DatabaseReader -> XPubSpec -> IO [XPubBal]+xPubBals r xpub = do+  Derived ext chg <- derivedAddrs r xpub+  bals <- scanXPubBals (getBalances r) r.initGap r.maxGap ext chg+  count r $ \s -> incrementCounter s.dataXPubBals (length bals)+  return bals++-- | Unspent outputs of an extended public key, newest first, given its+-- balances from 'xPubBals'.+--+-- The first unspent outputs of each address, at most offset plus limit of+-- them, sorted together. The addresses are read in step, so that each is+-- read only as far as the page needs, rather than decoding the whole quota+-- of every address to keep one page of it.+xPubUnspents :: DatabaseReader -> [XPubBal] -> Limits -> IO [XPubUnspent]+xPubUnspents r xbals limits = do+  let quota = case (deOffset limits).limit of+        0 -> Nothing+        l -> Just l+      stream b it =+        unspentConduit r b.balance.address limits.start it+          .| mapC (\u -> XPubUnspent {path = b.path, unspent = u})+  us <- withManyIters r (addrOutCF r.db) (map (AddrOutKeyA . (.balance.address)) cs) $ \its ->+    runConduit $+      mergeDescGroups (.unspent.block) (unspentKey . (.unspent)) quota (zipWith stream cs its)+        .| applyLimitsC limits+        .| sinkList+  count r $ \s -> incrementCounter s.dataXPubUnspents (length us)+  return us+  where+    cs = filter ((> 0) . (.balance.utxo)) xbals++-- | Transactions of an extended public key, newest first, given its balances+-- from 'xPubBals'.+xPubTxs :: DatabaseReader -> [XPubBal] -> Limits -> IO [TxRef]+xPubTxs r xbals limits = do+  txs <- getAddressesTxs r (usedAddresses xbals) limits+  count r $ \s -> incrementCounter s.dataXPubTxs (length txs)+  return txs++-- | Number of transactions of an extended public key, given its balances+-- from 'xPubBals'.+--+-- Each transaction is counted once even when it touches several addresses.+-- The entries of an address are sorted, and the same transaction has the same+-- key after the address prefix in every address, so the addresses are merged+-- by those raw key bytes and the distinct ones counted, without decoding any+-- key or reading any value.+xPubTxCount :: DatabaseReader -> [XPubBal] -> IO Word32+xPubTxCount r xbals = do+  count r $ \s -> incrementCounter s.dataXPubTxCount 1+  getAddressesTxCount r (usedAddresses xbals)++-- | Number of distinct transactions of a set of addresses: each transaction+-- is counted once even when it touches several of them. It reads only the+-- keys of the index, once each, as 'xPubTxCount' does, so it costs about as+-- much as reading the references of the transactions once, and much less+-- than paging through them.+getAddressesTxCount :: DatabaseReader -> [Address] -> IO Word32+getAddressesTxCount _ [] = return 0+getAddressesTxCount r as =+  withManyIters r (addrTxCF r.db) (map AddrTxKeyA as) $ \its ->+    fromIntegral <$> countDistinctSuffixes (zip (map (encode . AddrTxKeyA) as) its)++-- | Addresses of the balances that were ever used.+usedAddresses :: [XPubBal] -> [Address]+usedAddresses = map (.address) . filter (not . nullBalance) . map (.balance)++-- | Number of distinct keys, after their prefix, in a set of iterators that+-- each walk the keys with one prefix, in order.+countDistinctSuffixes :: [(BS.ByteString, Iterator)] -> IO Int+countDistinctSuffixes streams = do+  heads <- mapM start streams+  go 0 (Map.fromListWith (<>) [(k, [x]) | (k, x) <- catMaybes heads])+  where+    start x@(pfx, it) = iterSeek it pfx >> current x+    current x@(pfx, it) =+      iterKey it <&> \case+        Just k | pfx `BS.isPrefixOf` k -> Just (BS.drop (BS.length pfx) k, x)+        _ -> Nothing+    advance x@(_, it) = iterNext it >> current x+    go !n m = case Map.minViewWithKey m of+      Nothing -> return n+      Just ((_, xs), m') -> do+        nexts <- catMaybes <$> mapM advance xs+        go (n + 1) (foldr (\(k, x) -> Map.insertWith (<>) k [x]) m' nexts)++-------------------------------------------------------------------------------+-- Iterators and multi-gets+-------------------------------------------------------------------------------++-- | Run an action with an iterator on a column family, counting it in the+-- metrics.+--+-- The iterator is for the keys that start with a prefix, and RocksDB+-- stops it where they end ('scanOf').+withIterCFStat :: (Serialize k) => DatabaseReader -> ColumnFamily -> k -> (Iterator -> IO a) -> IO a+withIterCFStat r cf prefix f =+  bracket_ (gauge r 1) (gauge r (-1)) $ withIterConfig r.db (Just cf) (scanOf prefix) f++-- | An iterator for the keys that start with a prefix, such as those of one+-- address: one that RocksDB itself stops at the end of them.+--+-- A scan that stops by looking at the key that it has reached makes+-- RocksDB find that key first, and if the keys after the prefix were+-- deleted, it steps over the marker of every one of them to get there.+-- With an upper bound it stops at the bound. The summary of the mempool+-- area and the layout of the chain keep such runs of deleted keys short as+-- things are, so this is for the day that they do not: after a+-- reorganisation that reverts many blocks, or a change of layout.+scanOf :: (Serialize k) => k -> IterConfig+scanOf prefix = def {upperBound = prefixUpperBound (encode prefix)}++-- | Run an action with this many iterators on a column family. They are+-- opened and closed in a single bracket rather than one bracket each: each+-- bracket leaves a frame on the stack, and every call into RocksDB through+-- an iterator walks the stack, so with one bracket per address an xpub of a+-- hundred addresses made each step of each iterator a hundred times slower.+--+-- There is one iterator for each of the prefixes given, in their order,+-- and each is bounded to the keys of its prefix ('scanOf').+withManyIters :: (Serialize k) => DatabaseReader -> ColumnFamily -> [k] -> ([Iterator] -> IO a) -> IO a+withManyIters r cf prefixes f =+  bracket_ (gauge r i) (gauge r (negate i)) $+    bracket (newIORef []) (readIORef >=> sequence_) $ \ref -> do+      its <- forM prefixes $ \prefix -> mask_ $ do+        (it, destroy) <- createIteratorConfig r.db (Just cf) (scanOf prefix)+        modifyIORef' ref (destroy :)+        return it+      f its+  where+    i = length prefixes++-- | Change the gauge of open iterators.+gauge :: DatabaseReader -> Int -> IO ()+gauge r n = count r $ \s ->+  if n >= 0 then incrementGauge s.dataIters n else decrementGauge s.dataIters (negate n)++-- | Values of many keys of a column family, read with one multi-get, in+-- the order of the keys.+--+-- One key is read by itself. A multi-get costs about as much as a read to+-- set up and a third of one for each key, as measured, so it is the slower+-- way to read one key, and most requests about one transaction or one+-- address ask for one.+retrieveManyCF :: (Serialize key, Serialize value) => DB -> ColumnFamily -> [key] -> IO [Maybe value]+retrieveManyCF _ _ [] = return []+retrieveManyCF db cf [key] = pure . fmap (either error id . decode) <$> getCF db cf (encode key)+retrieveManyCF db cf keys =+  map (fmap (either error id . decode)) <$> multiGetCF db cf (map encode keys)++-- | Merge streams sorted newest first, drop duplicates, and apply the offset+-- and limit.+joinConduits :: (Ord k) => (o -> k) -> [ConduitT () o IO ()] -> Limits -> IO [o]+joinConduits key cs l =+  runConduit $ joinDescStreams key cs .| applyLimitsC l .| sinkList
+ src/Haskoin/Store/Database/Query/Layered.hs view
@@ -0,0 +1,807 @@+-- |+-- Module      : Haskoin.Store.Database.Query.Layered+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The queries of "Haskoin.Store.Database.Query" for the layered layout of+-- the database, in which the chain and the mempool are kept apart; see+-- @docs\/layered-database.md@. These are the queries that the store+-- answers with. Each answers as the query of the same name answers on a+-- database of version 20, pages included. The tests check that, on+-- databases migrated from that version and on databases built by the rules+-- of "Haskoin.Store.Logic.Layered".+--+-- = Two sources+--+-- A query reads the column families of the chain, with the functions of+-- "Haskoin.Store.Database.Query", reads what the mempool area has for the+-- same transaction, output or address, with those of+-- "Haskoin.Store.Database.Mempool", and puts the two together.+--+-- = Why a view+--+-- The two reads must see the database at the same moment. A block could+-- confirm a transaction between them. Read from the mempool before the block+-- and from the chain after it, the transaction would be listed twice, and+-- its payment counted twice in a balance; read the other way around, it+-- would be missing. The queries of version 20 need no such care, because each of their+-- answers is one key or one scan.+--+-- So every query takes a t'View', a snapshot of the database, and 'withView'+-- is the only way to get one: a query cannot be given the database itself by+-- mistake. The web server is meant to take one view for each request. That+-- also makes the several queries of a request agree with each other, which+-- they did not have to in version 20.+--+-- = Pages+--+-- Lists are ordered newest first, and unconfirmed transactions are newer+-- than any block. The list of an address is therefore what the mempool has+-- for it followed by what the chain has, and that is the sequence that the+-- single index of version 20 holds. A page is cut from it by the same+-- rules: where it starts, then the offset, then the limit. Both parts are+-- streamed, each from its own index, only as far as the page needs.+--+-- = Reading no more than the answer+--+-- Nearly every address has nothing in the mempool, and a few have thousands+-- of entries there. Each address that the mempool touches has a summary of+-- its entries, a t'MemSummary', which every query about an address reads+-- first, by key. A balance is the chain's balance with the summary applied.+-- A list reads the entries only of the addresses that have a summary, and+-- the summary says how many outputs the mempool spends, so that no more are+-- looked for. The first version of these queries scanned the entries of+-- every address for every question, which measured ten thousand times+-- slower for a busy address, and a hundred times slower for any address+-- after a block, when the scan steps over all that the block deleted.+module Haskoin.Store.Database.Query.Layered+  ( -- * A view of the database+    View,+    withView,+    layeredRead,+    standaloneRead,+    viewReader,++    -- * Parts shared with the queries of a fleet+    From (..),+    chainTxsC,+    chainUnspentsC,+    withSpenders,++    -- * Blocks+    getBestBlock,+    getBlocksAtHeight,+    getBlock,++    -- * Transactions+    getTxData,+    getManyTxData,+    getNumTxData,+    getSpender,++    -- * Addresses+    getBalance,+    getBalances,+    getUnspent,+    getAddressTxs,+    getAddressesTxs,+    getAddressUnspents,+    getAddressesUnspents,++    -- * Mempool+    getMempool,+    getMempoolPage,++    -- * Extended public keys+    xPubBals,+    xPubUnspents,+    xPubTxs,+    xPubTxCount,+    getAddressesTxCount,+  )+where++import Conduit+  ( ConduitT,+    await,+    awaitForever,+    concatMapC,+    lift,+    mapC,+    runConduit,+    sinkList,+    yield,+    yieldMany,+    (.|),+  )+import Control.Monad (forM)+import Data.Bits ((.&.))+import Data.ByteString qualified as BS+import Data.Functor ((<&>))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IntMap+import Data.List (partition)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, isJust)+import Data.Serialize (Serialize, encode)+import Data.Word (Word32, Word64)+import Database.RocksDB (ColumnFamily, Iterator, multiGetCF, withSnapshot)+import Database.RocksDB.Query+  ( matching,+    matchingAsListCF,+    matchingSkip,+    retrieveCF,+  )+import Haskoin+  ( Address,+    BlockHash,+    BlockHeight,+    OutPoint (..),+    Tx (..),+    TxHash,+    TxOut (..),+    eitherToMaybe,+    scriptToAddressBS,+  )+import Haskoin.Store.Common+  ( Limits (..),+    Start (..),+    StoreRead (..),+    applyLimitsC,+    deOffset,+    mergeDescGroups,+    notDeleted,+    scanXPubBals,+    txRefKey,+    unspentKey,+  )+import Haskoin.Store.Data+  ( Balance (..),+    BlockData,+    BlockRef (..),+    Spender,+    TxData (..),+    TxRef (..),+    UnixTime,+    Unspent (..),+    XPubBal (..),+    XPubSpec,+    XPubUnspent (..),+  )+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Reader+  ( DataMetrics (..),+    DatabaseReader (..),+    Derived (..),+    addrOutCF,+    addrTxCF,+    derivedAddrs,+    mempoolAddrCF,+    mempoolCF,+    txCF,+  )+import Haskoin.Store.Database.Types+  ( AddrOutKey (..),+    AddrTxKey (..),+    MemAddrKey (..),+    MemAddrVal (..),+    MemSpenderKey (..),+    MemSummary (..),+    OutVal,+    TxKey (..),+    decodeTxKey,+    toUnspent,+  )+import System.Metrics.StatsD (incrementCounter)++-------------------------------------------------------------------------------+-- A view of the database+-------------------------------------------------------------------------------++-- | A database of the layered layout as it was at one moment: both of its+-- areas, read through one RocksDB snapshot. Whatever is written after the+-- view was taken, the queries on it go on answering from that moment.+newtype View = View DatabaseReader++-- | The reader of a view: the database as it was at the moment of the+-- view. The queries of "Haskoin.Store.Database.Query" on it give what the+-- chain had then.+viewReader :: View -> DatabaseReader+viewReader (View r) = r++-- | Run an action with a view of a database as it is now.+--+-- Taking a view costs about a microsecond, so there is no reason to share+-- one between requests, and a reason not to: RocksDB keeps the data that a+-- view can see for as long as the view is open.+--+-- The view, and anything made from it, such as the queries of 'layeredRead',+-- must not be used after the action returns: the snapshot is released then,+-- and RocksDB would be handed a pointer to freed memory. An action that+-- gives back something lazy or something that reads later, such as the body+-- of a streamed response, has to run inside.+withView :: DatabaseReader -> (View -> IO a) -> IO a+withView r action = withSnapshot r.db $ \snapshot -> action (View r {db = snapshot})++-- | The queries of a view as a t'StoreRead', for the web server, as+-- 'Q.databaseRead' gives those of a database of version 20.+layeredRead :: View -> StoreRead+layeredRead v@(View r) =+  StoreRead+    { net = r.net,+      ctx = r.ctx,+      getBestBlock = getBestBlock v,+      getBlocksAtHeight = getBlocksAtHeight v,+      getBlock = getBlock v,+      getTxData = fmap notDeleted . getTxData v,+      getManyTxData = fmap (map notDeleted) . getManyTxData v,+      getSpender = getSpender v,+      getBalance = getBalance v,+      getUnspent = getUnspent v,+      getMempool = getMempool v,+      getMempoolPage = getMempoolPage v,+      getAddressesTxs = getAddressesTxs v,+      getAddressesUnspents = getAddressesUnspents v,+      initialGap = r.initGap,+      maxGap = r.maxGap,+      getNumTxData = fmap (filter (not . (.deleted))) . getNumTxData v,+      getBalances = getBalances v,+      getAddressTxs = getAddressTxs v,+      getAddressUnspents = getAddressUnspents v,+      xPubBals = xPubBals v,+      xPubUnspents = \_ -> xPubUnspents v,+      xPubTxs = \_ -> xPubTxs v,+      xPubTxCount = \_ -> xPubTxCount v,+      getAddressesTxCount = getAddressesTxCount v+    }++-- | The queries of a database as a t'StoreRead', each on a view of its+-- own, taken when it is asked. For code that asks one thing at a time and+-- has no request to hold a view for. Queries asked one after the other may+-- see different moments, so what needs several of them to agree takes a+-- view with 'withView'.+standaloneRead :: DatabaseReader -> StoreRead+standaloneRead r =+  StoreRead+    { net = r.net,+      ctx = r.ctx,+      getBestBlock = one (.getBestBlock),+      getBlocksAtHeight = \h -> one (\q -> q.getBlocksAtHeight h),+      getBlock = \h -> one (\q -> q.getBlock h),+      getTxData = \h -> one (\q -> q.getTxData h),+      getManyTxData = \hs -> one (\q -> q.getManyTxData hs),+      getSpender = \p -> one (\q -> q.getSpender p),+      getBalance = \a -> one (\q -> q.getBalance a),+      getUnspent = \p -> one (\q -> q.getUnspent p),+      getMempool = one (.getMempool),+      getMempoolPage = \l -> one (\q -> q.getMempoolPage l),+      getAddressesTxs = \as l -> one (\q -> q.getAddressesTxs as l),+      getAddressesUnspents = \as l -> one (\q -> q.getAddressesUnspents as l),+      initialGap = r.initGap,+      maxGap = r.maxGap,+      getNumTxData = \n -> one (\q -> q.getNumTxData n),+      getBalances = \as -> one (\q -> q.getBalances as),+      getAddressTxs = \a l -> one (\q -> q.getAddressTxs a l),+      getAddressUnspents = \a l -> one (\q -> q.getAddressUnspents a l),+      xPubBals = \x -> one (\q -> q.xPubBals x),+      xPubUnspents = \x bs l -> one (\q -> q.xPubUnspents x bs l),+      xPubTxs = \x bs l -> one (\q -> q.xPubTxs x bs l),+      xPubTxCount = \x bs -> one (\q -> q.xPubTxCount x bs),+      getAddressesTxCount = \as -> one (\q -> q.getAddressesTxCount as)+    }+  where+    one :: (StoreRead -> IO a) -> IO a+    one f = withView r (f . layeredRead)++-------------------------------------------------------------------------------+-- Blocks+-------------------------------------------------------------------------------++-- | Hash of the best block, if the database has any.+getBestBlock :: View -> IO (Maybe BlockHash)+getBestBlock (View r) = Q.getBestBlock r++-- | Hashes of the blocks at a height, of the main chain and of side chains.+getBlocksAtHeight :: View -> BlockHeight -> IO [BlockHash]+getBlocksAtHeight (View r) = Q.getBlocksAtHeight r++-- | Data of a block.+getBlock :: View -> BlockHash -> IO (Maybe BlockData)+getBlock (View r) = Q.getBlock r++-------------------------------------------------------------------------------+-- Transactions+-------------------------------------------------------------------------------++-- | Data of a transaction, with the spenders of its outputs, those in blocks+-- and those in the mempool. A transaction in the mempool comes from the+-- mempool area, and a confirmed one from the chain. One that left the+-- mempool unconfirmed is not known: nothing is kept of it, and the bodies+-- that version 20 left in the chain for such transactions are passed over.+getTxData :: View -> TxHash -> IO (Maybe TxData)+getTxData v h =+  getManyTxData v [h] <&> \case+    [found] -> found+    _ -> Nothing++-- | Data of many transactions, in the order of the hashes. Each column+-- family that has to be read is read with one multi-get.+getManyTxData :: View -> [TxHash] -> IO [Maybe TxData]+getManyTxData (View r) hs = do+  found <- bodies r hs+  spent <- mempoolSpenders r (Map.toList found)+  let tds = [withSpenders spent h <$> Map.lookup h found | h <- hs]+  Q.count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes tds))+  return tds++-- | Transactions whose hash, read as a number, matches a short identifier of+-- the Blockchain.info API, in the order of their hashes. There may be more+-- than one.+getNumTxData :: View -> Word64 -> IO [TxData]+getNumTxData (View r) i = do+  unconfirmed <- matchingAsListCF r.db (mempoolCF r.db) (TxKeyS prefix)+  stored <- matchingAsListCF r.db (txCF r.db) (TxKeyS prefix)+  let rows = [(h, td) | (TxKey h, td) <- stored]+  chain <- zip (map fst rows) <$> Q.withChainSpenders r rows+  let found =+        preferred+          (Map.fromList [(h, td) | (TxKey h, td) <- unconfirmed])+          (Map.fromList chain)+      matches = filter (seventh . fst) (Map.toAscList found)+  spent <- mempoolSpenders r matches+  Q.count r $ \s -> incrementCounter s.dataTxCount (length matches)+  return [withSpenders spent h td | (h, td) <- matches]+  where+    (prefix, bits) = decodeTxKey i+    -- the keys match on the first six bytes of the hash; the identifier+    -- also has the five high bits of the seventh+    seventh h = BS.index (encode h) 6 .&. 0xf8 == bits++-- | Input that spends an output, in the mempool or in a block.+getSpender :: View -> OutPoint -> IO (Maybe Spender)+getSpender (View r) p =+  retrieveCF r.db (mempoolCF r.db) (MemSpenderKey p) >>= \case+    Just s -> return (Just s)+    Nothing -> Q.getChainSpender r p++-- | The bodies of transactions by hash, each from where it is kept, with+-- the spenders that blocks give to its outputs and without those of the+-- mempool.+--+-- The mempool area is asked first, although most transactions asked for are+-- confirmed: with its filter, finding that a transaction is not there costs+-- a fraction of reading a body, and the chain is then asked only for the+-- others.+bodies :: DatabaseReader -> [TxHash] -> IO (Map TxHash TxData)+bodies r hs = do+  unconfirmed <- look (mempoolCF r.db) TxKey hs+  chain <- look (txCF r.db) TxKey (filter (`Map.notMember` unconfirmed) hs) >>= spent+  return (preferred unconfirmed chain)+  where+    -- the spenders that blocks give to the outputs of confirmed+    -- transactions are under keys of their own+    spent found = do+      let rows = Map.toAscList found+      Map.fromDistinctAscList . zip (map fst rows) <$> Q.withChainSpenders r rows+    look :: (Serialize key) => ColumnFamily -> (TxHash -> key) -> [TxHash] -> IO (Map TxHash TxData)+    look _ _ [] = return Map.empty+    look cf key ks =+      Map.fromList . catMaybes . zipWith (fmap . (,)) ks+        <$> Q.retrieveManyCF r.db cf (map key ks)++-- | The body to answer with for each transaction, given the bodies that the+-- mempool area has for transactions in the mempool and those that the+-- chain has. A transaction is in one place only. A body that version 20+-- left in the chain for a transaction that it deleted is no body at all:+-- finding them all to delete them would mean reading every transaction of+-- the database, so they stay where they are and are passed over.+preferred :: Map TxHash TxData -> Map TxHash TxData -> Map TxHash TxData+preferred unconfirmed chain = unconfirmed <> Map.filter (not . (.deleted)) chain++-- | The spenders that the mempool has for the outputs of transactions,+-- given their bodies as 'bodies' gives them.+--+-- Only the outputs that no block spends are looked up, since the mempool+-- cannot spend the others: all of them with one multi-get, which the filter+-- of the column family answers for the many outputs that the mempool does+-- not spend. A transaction with more than 'scanFrom' such outputs has its+-- spenders read with a scan of their common prefix instead.+mempoolSpenders :: DatabaseReader -> [(TxHash, TxData)] -> IO (HashMap TxHash (IntMap Spender))+mempoolSpenders r txs = do+  found <- case points of+    [] -> return []+    _ -> Q.retrieveManyCF r.db (mempoolCF r.db) (map MemSpenderKey points)+  scanned <- mapM (\(h, _) -> (h,) <$> M.getMempoolSpenders r h) wide+  return . HashMap.fromListWith IntMap.union $+    [(p.hash, IntMap.singleton (fromIntegral p.index) s) | (p, Just s) <- zip points found]+      <> scanned+  where+    open :: [(TxHash, [Word32])]+    open =+      [ (h, [i | (i, _) <- zip [0 ..] td.tx.outputs, IntMap.notMember (fromIntegral i) td.spenders])+      | (h, td) <- txs,+        not td.deleted+      ]+    (narrow, wide) = partition (null . drop scanFrom . snd) open+    points = [OutPoint h i | (h, is) <- narrow, i <- is]++-- | Number of outputs of a transaction above which the mempool spenders of+-- the transaction are read with a scan. A scan costs about as much as eight+-- lookups in a multi-get whatever it finds, by the measurements in+-- @docs\/layered-database.md@.+scanFrom :: Int+scanFrom = 8++-- | Add the spenders that the mempool has for the outputs of a transaction+-- to those in its body.+withSpenders :: HashMap TxHash (IntMap Spender) -> TxHash -> TxData -> TxData+withSpenders spent h td = case HashMap.lookup h spent of+  Nothing -> td+  Just unconfirmed -> (td :: TxData) {spenders = IntMap.union td.spenders unconfirmed}++-------------------------------------------------------------------------------+-- Addresses+-------------------------------------------------------------------------------++-- | Balance of an address, with the mempool applied. 'Nothing' if neither+-- the chain nor the mempool has anything for the address; see+-- 'M.mergeBalance' for where version 20 says zero instead.+--+-- It is two reads by key, the balance of the chain and the summary of the+-- mempool, however much the mempool does at the address.+getBalance :: View -> Address -> IO (Maybe Balance)+getBalance (View r) a = M.summaryBalance a <$> Q.getBalance r a <*> (only <$> M.getSummaries r [a])+  where+    only = \case+      [s] -> s+      _ -> Nothing++-- | Balances of addresses, in the same order, with the mempool applied.+-- Addresses never used get a zero balance. Two multi-gets, one in each area.+getBalances :: View -> [Address] -> IO [Balance]+getBalances (View r) as = zipWith apply <$> Q.getBalances r as <*> M.getSummaries r as+  where+    apply c = maybe c (M.applySummary c)++-- | Unspent output, if it exists and neither a block nor the mempool spends+-- it: one that the chain has, or one of a mempool transaction.+getUnspent :: View -> OutPoint -> IO (Maybe Unspent)+getUnspent (View r) p =+  retrieveCF r.db (mempoolCF r.db) (MemSpenderKey p) >>= \case+    Just (_ :: Spender) -> return Nothing+    Nothing ->+      Q.getUnspent r p >>= \case+        Just u -> return (Just u)+        Nothing -> (>>= output) <$> M.getMempoolTx r p.hash+  where+    output td = case drop (fromIntegral p.index) td.tx.outputs of+      o : _ ->+        Just+          Unspent+            { block = td.block,+              outpoint = p,+              value = o.value,+              script = o.script,+              address = eitherToMaybe (scriptToAddressBS r.net r.ctx o.script)+            }+      [] -> Nothing++-- | Transactions of an address, newest first.+getAddressTxs :: View -> Address -> Limits -> IO [TxRef]+getAddressTxs v a = getAddressesTxs v [a]++-- | Transactions of any of the addresses, newest first, each once.+getAddressesTxs :: View -> [Address] -> Limits -> IO [TxRef]+getAddressesTxs (View r) as limits = paged r limits $ \from -> do+  txs <- withMempoolIters r from as (const True) $ \mempools ->+    Q.withManyIters r (addrTxCF r.db) (map AddrTxKeyA as) $ \its ->+      Q.joinConduits txRefKey (zipWith3 (addressTxsC from) as (map (fmap snd) mempools) its) limits+  Q.count r $ \s -> incrementCounter s.dataAddrTxCount (length txs)+  return txs++-- | Unspent outputs of an address, newest first.+getAddressUnspents :: View -> Address -> Limits -> IO [Unspent]+getAddressUnspents v a = getAddressesUnspents v [a]++-- | Unspent outputs of any of the addresses, newest first.+getAddressesUnspents :: View -> [Address] -> Limits -> IO [Unspent]+getAddressesUnspents (View r) as limits = paged r limits $ \from -> do+  us <- withUnspentStreams r from as $ \streams ->+    Q.joinConduits unspentKey streams limits+  Q.count r $ \s -> incrementCounter s.dataUnspentCount (length us)+  return us++-------------------------------------------------------------------------------+-- Pages+-------------------------------------------------------------------------------++-- | Where a page starts in a list that has the mempool's part first and the+-- chain's part after it.+data From+  = -- | at the newest item+    Top+  | -- | in the mempool's part, at the transaction that entered the mempool+    -- at this time and has this hash, or where it would be+    InMempool !UnixTime !TxHash+  | -- | in the chain's part, at this position: nothing of the mempool is in+    -- the page+    InChain !BlockRef++-- | Where the page of a query starts, or 'Nothing' if it starts at a+-- transaction that is not known, which makes the page empty.+--+-- A transaction that left the mempool unconfirmed is not known, so a page+-- that starts at one is empty.+pageStart :: DatabaseReader -> Maybe Start -> IO (Maybe From)+pageStart _ Nothing = return (Just Top)+pageStart _ (Just (AtBlock height)) = return (Just (InChain (BlockRef height maxBound)))+pageStart r (Just (AtTx h)) =+  bodies r [h] <&> \found -> case (.block) <$> Map.lookup h found of+    Nothing -> Nothing+    Just (MemRef time) -> Just (InMempool time h)+    Just position@BlockRef {} -> Just (InChain position)++-- | Answer a query from where its page starts.+paged :: DatabaseReader -> Limits -> (From -> IO [a]) -> IO [a]+paged r limits answer = pageStart r limits.start >>= maybe (return []) answer++-- | Run an action with, for each address, its summary and an iterator on+-- its entries in the mempool area, or 'Nothing' for an address whose+-- entries the list does not need: one that the mempool does not touch, which+-- is nearly all of them, one whose summary says that there is nothing of+-- the kind wanted, and any address when the page starts in the chain, as+-- every page after the first of a long history does.+--+-- The summaries are read by key in one multi-get, which the filter of the+-- column family answers for the addresses that have none. No iterator is+-- moved to an address with nothing there: such a seek costs ten times a+-- read by key, and far more when it has to step over what a block just+-- deleted.+withMempoolIters ::+  DatabaseReader ->+  From ->+  [Address] ->+  (MemSummary -> Bool) ->+  ([Maybe (MemSummary, Iterator)] -> IO a) ->+  IO a+withMempoolIters r from as wanted action = do+  summaries <- case from of+    InChain _ -> return (map (const Nothing) as)+    _ -> map (>>= \s -> if wanted s then Just s else Nothing) <$> M.getSummaries r as+  Q.withManyIters r (mempoolAddrCF r.db) [MemAddrKeyA a | (a, Just _) <- zip as summaries] (action . pair summaries)+  where+    pair (Just s : rest) (it : its) = Just (s, it) : pair rest its+    pair (_ : rest) its = Nothing : pair rest its+    pair [] _ = []++-- | Transactions of an address, newest first, from where a page starts:+-- those of the mempool that belong in it, streamed from the entries of the+-- address if it has any, then those of the chain, streamed from its index.+-- Neither is read further than the page goes.+addressTxsC :: From -> Address -> Maybe Iterator -> Iterator -> ConduitT i TxRef IO ()+addressTxsC from a mempool it = mapM_ (mempoolTxsC from a) mempool >> chainTxsC from a it++-- | Transactions of an address in the chain, newest first, from where a+-- page starts, streamed from the index of the address.+chainTxsC :: From -> Address -> Iterator -> ConduitT i TxRef IO ()+chainTxsC from a it = chain .| concatMapC ref+  where+    chain = case from of+      InChain position -> matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a position)+      _ -> matching it (AddrTxKeyA a)+    ref :: (AddrTxKey, ()) -> Maybe TxRef+    ref (AddrTxKey _ t, ()) = Just t+    ref _ = Nothing++-- | Mempool transactions of an address, newest first, from where a page+-- starts. Those that entered the mempool in the same second are in the+-- order of their hashes, which is the order of their keys.+mempoolTxsC :: From -> Address -> Iterator -> ConduitT i TxRef IO ()+mempoolTxsC from a it = rows .| concatMapC ref+  where+    rows = case from of+      Top -> matching it (MemAddrTxKeyA a)+      InMempool time h -> matchingSkip it (MemAddrTxKeyA a) (MemAddrTxKey a time h)+      InChain _ -> return ()+    ref :: (MemAddrKey, MemAddrVal) -> Maybe TxRef+    ref (MemAddrTxKey _ t h, _) = Just TxRef {block = MemRef t, txid = h}+    ref _ = Nothing++-- | Run an action with the stream of unspent outputs of each address, from+-- where a page starts.+--+-- The summaries are read whatever the start of the page: the chain still+-- has, as unspent, the outputs that the mempool spends, on every page.+withUnspentStreams ::+  DatabaseReader ->+  From ->+  [Address] ->+  ([ConduitT i Unspent IO ()] -> IO a) ->+  IO a+withUnspentStreams r from as action = do+  summaries <- M.getSummaries r as+  let fresh = case from of+        InChain _ -> map (const Nothing) as+        _ -> map (>>= \s -> if s.paidCount > s.bothCount then Just s else Nothing) summaries+  Q.withManyIters r (mempoolAddrCF r.db) [MemAddrKeyA a | (a, Just _) <- zip as fresh] $ \mits ->+    Q.withManyIters r (addrOutCF r.db) (map AddrOutKeyA as) $ \its ->+      action (zipWith3 stream (zip as summaries) (pair fresh mits) its)+  where+    stream (a, s) = addressUnspentsC r from a s+    pair (Just _ : rest) (it : its) = Just it : pair rest its+    pair (_ : rest) its = Nothing : pair rest its+    pair [] _ = []++-- | Unspent outputs of an address, newest first, from where a page starts,+-- given the summary of what the mempool does at the address, if anything,+-- and an iterator on its entries if the page has some: the outputs that the+-- mempool created and has not spent, then those of the chain that the+-- mempool does not spend, streamed from the index of the address.+--+-- Whether the mempool spends an output is a read by key, and the summary+-- says how many of them there are to find: none for nearly every address,+-- so that nothing is read, and when the last one is found the reads stop.+addressUnspentsC ::+  DatabaseReader ->+  From ->+  Address ->+  Maybe MemSummary ->+  Maybe Iterator ->+  Iterator ->+  ConduitT i Unspent IO ()+addressUnspentsC r from a summary mempool it = do+  mapM_ (\mit -> fresh mit .| concatMapC created .| hide (maybe 0 (.bothCount) summary) 1) mempool+  chainUnspentsC r from a it .| hide (maybe 0 (\s -> s.spentCount - s.bothCount) summary) 1+  where+    -- the outputs of the transaction that the page starts at come first+    -- among those of the same second whose hashes are not smaller+    fresh mit = case from of+      Top -> matching mit (MemAddrOutKeyA a)+      InMempool time h -> matchingSkip mit (MemAddrOutKeyA a) (MemAddrOutKey a time (OutPoint h 0))+      InChain _ -> return ()+    created :: (MemAddrKey, MemAddrVal) -> Maybe Unspent+    created (MemAddrOutKey _ t p, MemAddrOutVal v s) =+      Just Unspent {block = MemRef t, outpoint = p, value = v, script = s, address = Just a}+    created _ = Nothing+    -- let outputs through, but for those that the mempool spends, of which+    -- there are no more than a number. They are asked about several at a+    -- time, in one multi-get, which costs a fifth of asking about each: one+    -- at first, then twice as many each time. A page of one address is then+    -- a few reads, and the first output of each of the hundreds of+    -- addresses of a wallet, which is all that a merge of them may take,+    -- does not cost a batch of rows read ahead for each.+    hide :: Word64 -> Int -> ConduitT Unspent Unspent IO ()+    hide 0 _ = awaitForever yield+    hide n size =+      gather size >>= \case+        [] -> return ()+        batch -> do+          found <- lift (spentAmong batch)+          yieldMany [u | (u, False) <- zip batch found]+          hide (n - min n (fromIntegral (length (filter id found)))) (min hideBatch (size * 2))+    -- whether the mempool spends each of some outputs of the address: a+    -- read by key for each of a few, and one multi-get for more, which+    -- only then costs less+    spentAmong :: [Unspent] -> IO [Bool]+    spentAmong batch+      | length batch < 8 = forM batch $ \u -> do+          found <- retrieveCF r.db (mempoolAddrCF r.db) (MemAddrSpentKey a u.outpoint)+          return (isJust (found :: Maybe MemAddrVal))+      | otherwise =+          map isJust <$> multiGetCF r.db (mempoolAddrCF r.db) [encode (MemAddrSpentKey a u.outpoint) | u <- batch]+    gather :: Int -> ConduitT Unspent o IO [Unspent]+    gather 0 = return []+    gather k =+      await >>= \case+        Nothing -> return []+        Just u -> (u :) <$> gather (k - 1)++-- | The most outputs that are asked about at once when looking for those+-- that the mempool spends. More would save little, and each one is a row+-- read ahead of what a page may need.+hideBatch :: Int+hideBatch = 32++-- | Unspent outputs of an address in the chain, newest first, from where a+-- page starts, streamed from the index of the address. Those that the+-- mempool spends are among them.+chainUnspentsC :: DatabaseReader -> From -> Address -> Iterator -> ConduitT i Unspent IO ()+chainUnspentsC r from a it = chain .| concatMapC output+  where+    chain = case from of+      InChain position -> matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a position)+      _ -> matching it (AddrOutKeyA a)+    output :: (AddrOutKey, OutVal) -> Maybe Unspent+    output = uncurry (toUnspent r.net r.ctx)++-------------------------------------------------------------------------------+-- Mempool+-------------------------------------------------------------------------------++-- | The whole mempool, newest first.+getMempool :: View -> IO [(UnixTime, TxHash)]+getMempool (View r) = Q.getMempool r++-- | Page of the mempool, newest first, from the offset and limit of the+-- t'Limits'; its start is ignored. The list of the mempool and its index by+-- time are the same in both layouts.+getMempoolPage :: View -> Limits -> IO [(UnixTime, TxHash)]+getMempoolPage (View r) = Q.getMempoolPage r++-------------------------------------------------------------------------------+-- Extended public keys+-------------------------------------------------------------------------------++-- | Balances of the addresses of an extended public key, derived up to the+-- exact gap (see 'scanXPubBals'), with the mempool applied. An address that+-- only the mempool has used counts as used, as it did in version 20.+xPubBals :: View -> XPubSpec -> IO [XPubBal]+xPubBals v@(View r) xpub = do+  Derived ext chg <- derivedAddrs r xpub+  bals <- scanXPubBals (getBalances v) r.initGap r.maxGap ext chg+  Q.count r $ \s -> incrementCounter s.dataXPubBals (length bals)+  return bals++-- | Unspent outputs of an extended public key, newest first, given its+-- balances from @xPubBals@. The addresses are read in step, each only as far+-- as the page needs, as 'Q.xPubUnspents' reads them.+xPubUnspents :: View -> [XPubBal] -> Limits -> IO [XPubUnspent]+xPubUnspents (View r) xbals limits = paged r limits $ \from -> do+  let quota = case (deOffset limits).limit of+        0 -> Nothing+        l -> Just l+      tag b = mapC (\u -> XPubUnspent {path = b.path, unspent = u})+  us <- withUnspentStreams r from (map (.balance.address) funded) $ \streams ->+    runConduit $+      mergeDescGroups (.unspent.block) (unspentKey . (.unspent)) quota (zipWith (\b s -> s .| tag b) funded streams)+        .| applyLimitsC limits+        .| sinkList+  Q.count r $ \s -> incrementCounter s.dataXPubUnspents (length us)+  return us+  where+    funded = filter ((> 0) . (.balance.utxo)) xbals++-- | Transactions of an extended public key, newest first, given its balances+-- from @xPubBals@.+xPubTxs :: View -> [XPubBal] -> Limits -> IO [TxRef]+xPubTxs v@(View r) xbals limits = do+  txs <- getAddressesTxs v (Q.usedAddresses xbals) limits+  Q.count r $ \s -> incrementCounter s.dataXPubTxs (length txs)+  return txs++-- | Number of transactions of an extended public key, given its balances+-- from @xPubBals@.+xPubTxCount :: View -> [XPubBal] -> IO Word32+xPubTxCount v@(View r) xbals = do+  Q.count r $ \s -> incrementCounter s.dataXPubTxCount 1+  getAddressesTxCount v (Q.usedAddresses xbals)++-- | Number of distinct transactions of a set of addresses: each transaction+-- is counted once even when it touches several of them.+--+-- Those of the chain are counted from the keys of its index, as+-- 'Q.getAddressesTxCount' counts them, and those of the mempool from the+-- summaries of the addresses: nothing more is read when none of them, or+-- only one, has transactions in the mempool, and otherwise the transactions+-- of those that have are read, to count each once. No transaction is in+-- both areas.+getAddressesTxCount :: View -> [Address] -> IO Word32+getAddressesTxCount _ [] = return 0+getAddressesTxCount (View r) as = do+  inBlocks <- Q.getAddressesTxCount r as+  inMempool <- withMempoolIters r Top as ((> 0) . (.txs)) $ \mempools ->+    case [(a, m) | (a, Just m) <- zip as mempools] of+      [] -> return 0+      [(_, (s, _))] -> return (fromIntegral s.txs)+      several -> do+        txs <- forM several $ \(a, (_, it)) -> runConduit (mempoolTxsC Top a it .| mapC (.txid) .| sinkList)+        return (HashSet.size (HashSet.fromList (concat txs)))+  return (inBlocks + fromIntegral inMempool)
+ src/Haskoin/Store/Database/Query/Pooled.hs view
@@ -0,0 +1,518 @@+-- |+-- Module      : Haskoin.Store.Database.Query.Pooled+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The queries of the store with a mempool that is not the store's own laid+-- over the chain: the shared pool of a fleet of stores, which+-- "Haskoin.Store.Fleet.Redis" keeps in Redis. See "A fleet that answers as+-- one" in @docs\/layered-database.md@.+--+-- = Why another set of queries+--+-- "Haskoin.Store.Database.Query.Layered" answers from the two areas of one+-- database, read through one snapshot. Stores that share a Redis are to+-- give the same answer to the same request, and their own mempools differ.+-- So each answers from the chain as it was at the block that the fleet+-- agreed on, which every store has the same, and from the one pool that+-- they share. The chain's part of an answer is read as the layered queries+-- read it, with the same code. The mempool's part comes through a+-- t'PoolReads', a handful of functions that say what a pool has: its+-- summaries, the entries of an address by the page, bodies, spenders and+-- its list.+--+-- The alternative was to give the layered queries the same handful of+-- functions for the store's own mempool area. They read that area through+-- iterators that are made once for a request and moved from address to+-- address, which is where their speed comes from, and which a function+-- that returns a page would hide. The two sets share what is the same, and+-- the tests hold this one to the answers of the other: 'localPool' is the+-- store's own mempool area as a t'PoolReads', and with it every query here+-- must answer as the layered one does.+--+-- = What a pool cannot say+--+-- A transaction that left a mempool unconfirmed is kept by the store that+-- deleted it, and by no other. A fleet cannot answer the same about it from+-- every store, so these queries do not know deleted transactions at all: a+-- page that starts at one is empty.+module Haskoin.Store.Database.Query.Pooled+  ( PoolReads (..),+    pooledRead,+    localPool,+    pageStart,+  )+where++import Conduit+  ( ConduitT,+    await,+    awaitForever,+    lift,+    mapC,+    runConduit,+    sinkList,+    yield,+    yieldMany,+    (.|),+  )+import Control.Monad (forM)+import Data.Bits ((.&.))+import Data.ByteString qualified as BS+import Data.Functor ((<&>))+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict qualified as IntMap+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, isJust)+import Data.Serialize (encode)+import Data.Word (Word16, Word32, Word64)+import Database.RocksDB (Iterator)+import Database.RocksDB.Query (matchingAsListCF, retrieveCF)+import Haskoin+  ( Address,+    OutPoint (..),+    Tx (..),+    TxHash,+    TxOut (..),+    eitherToMaybe,+    scriptToAddressBS,+  )+import Haskoin.Store.Common+  ( Limits (..),+    Start (..),+    StoreRead (..),+    applyLimitsC,+    deOffset,+    mergeDescGroups,+    notDeleted,+    scanXPubBals,+    txRefKey,+    unspentKey,+  )+import Haskoin.Store.Data+  ( Balance (..),+    BlockRef (..),+    Spender,+    TxData (..),+    TxRef (..),+    UnixTime,+    Unspent (..),+    XPubBal (..),+    XPubUnspent (..),+  )+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered (From (..), chainTxsC, chainUnspentsC, withSpenders)+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    Derived (..),+    addrOutCF,+    addrTxCF,+    derivedAddrs,+    mempoolAddrCF,+    mempoolCF,+    txCF,+  )+import Haskoin.Store.Database.Types+  ( AddrOutKey (..),+    AddrTxKey (..),+    MemAddrKey (..),+    MemAddrVal (..),+    MemSpenderKey (..),+    MemSummary (..),+    TxKey (..),+    decodeTxKey,+  )++-- | What a mempool has, as much as the queries need of it. Each function+-- answers about the pool as it is when asked: one that goes with a block+-- must fail, by throwing, when the pool has moved to another.+data PoolReads = PoolReads+  { -- | the summary of each address, in their order, or 'Nothing' for an+    -- address that the pool does not touch+    summaries :: [Address] -> IO [Maybe MemSummary],+    -- | transactions of an address, newest first and in the order of their+    -- hashes within a second, from the one that entered at a time with a+    -- hash, or where it would be, or from the newest; at most so many+    addressTxs :: Address -> Maybe (UnixTime, TxHash) -> Int -> IO [(UnixTime, TxHash)],+    -- | outputs that the pool pays to an address, in the same order, from+    -- one of a time or from the newest, at most so many, each with whether+    -- the pool also spends it+    addressOuts :: Address -> Maybe (UnixTime, OutPoint) -> Int -> IO [(Unspent, Bool)],+    -- | whether the pool spends each of some outputs of an address+    spentOutputs :: Address -> [OutPoint] -> IO [Bool],+    -- | for each of some transactions, in their order: its body if it is+    -- in the pool, without spenders, and the inputs of the pool that spend+    -- its outputs, by output, whether it is in the pool or in the chain.+    -- The two come together because nearly every question about a+    -- transaction needs both, and a pool that is far away answers them in+    -- one go.+    bodies :: [TxHash] -> IO [(Maybe TxData, IntMap.IntMap Spender)],+    -- | the input of the pool that spends each output, if one does+    spenders :: [OutPoint] -> IO [Maybe Spender],+    -- | the list of the pool, newest first and by hash downwards within a+    -- second, from an offset, at most so many, or all of it for 0+    page :: Word32 -> Word32 -> IO [(UnixTime, TxHash)],+    -- | the transactions of the pool whose hash starts with six bytes, as+    -- 'decodeTxKey' gives them+    startingWith :: (Word32, Word16) -> IO [TxHash]+  }++-- | The queries of a store that answers from the chain as a reader has it,+-- which is to be the view of the database at a block, with a pool on top.+pooledRead :: DatabaseReader -> PoolReads -> StoreRead+pooledRead r pool =+  StoreRead+    { net = r.net,+      ctx = r.ctx,+      getBestBlock = Q.getBestBlock r,+      getBlocksAtHeight = Q.getBlocksAtHeight r,+      getBlock = Q.getBlock r,+      getTxData = \h -> only <$> getManyTxData r pool [h],+      getManyTxData = getManyTxData r pool,+      getSpender = getSpender r pool,+      getBalance = getBalance r pool,+      getUnspent = getUnspent r pool,+      getMempool = pool.page 0 0,+      getMempoolPage = \l -> pool.page l.offset l.limit,+      getAddressesTxs = getAddressesTxs r pool,+      getAddressesUnspents = getAddressesUnspents r pool,+      initialGap = r.initGap,+      maxGap = r.maxGap,+      getNumTxData = getNumTxData r pool,+      getBalances = getBalances r pool,+      getAddressTxs = \a -> getAddressesTxs r pool [a],+      getAddressUnspents = \a -> getAddressesUnspents r pool [a],+      xPubBals = \xpub -> do+        Derived ext chg <- derivedAddrs r xpub+        scanXPubBals (getBalances r pool) r.initGap r.maxGap ext chg,+      xPubUnspents = \_ -> xPubUnspents r pool,+      xPubTxs = \_ xbals -> getAddressesTxs r pool (Q.usedAddresses xbals),+      xPubTxCount = \_ xbals -> getAddressesTxCount r pool (Q.usedAddresses xbals),+      getAddressesTxCount = getAddressesTxCount r pool+    }+  where+    only = \case+      [found] -> found+      _ -> Nothing++-------------------------------------------------------------------------------+-- Transactions+-------------------------------------------------------------------------------++-- | Data of many transactions, in the order of the hashes: those of the+-- pool from it, the others from the chain, each with the spenders that+-- blocks and the pool give to its outputs. Deleted ones are not known.+getManyTxData :: DatabaseReader -> PoolReads -> [TxHash] -> IO [Maybe TxData]+getManyTxData _ _ [] = return []+getManyTxData r pool hs = do+  found <- known r pool hs+  return [Map.lookup h found | h <- hs]++-- | The data of transactions by hash: those that the pool has, and those+-- that the chain has for the others, each with the spenders that blocks+-- and the pool give to its outputs. One read of the pool, and one of the+-- chain for what the pool does not have.+known :: DatabaseReader -> PoolReads -> [TxHash] -> IO (Map TxHash TxData)+known _ _ [] = return Map.empty+known r pool hs = do+  inPool <- zip hs <$> pool.bodies hs+  let unconfirmed = Map.fromList [(h, td) | (h, (Just td, _)) <- inPool]+      spent = HashMap.fromList [(h, spenders) | (h, (_, spenders)) <- inPool, not (IntMap.null spenders)]+      rest = filter (`Map.notMember` unconfirmed) hs+  chain <- case rest of+    [] -> return Map.empty+    _ -> do+      found <- catMaybes . zipWith (fmap . (,)) rest <$> Q.retrieveManyCF r.db (txCF r.db) (map TxKey rest)+      -- with the spenders that blocks give to their outputs+      Map.fromList . zip (map fst found) <$> Q.withChainSpenders r found+  return (Map.mapWithKey (withSpenders spent) (unconfirmed <> Map.mapMaybe notDeleted' chain))+  where+    notDeleted' = notDeleted . Just++-- | Transactions whose hash, read as a number, matches a short identifier+-- of the Blockchain.info API, in the order of their hashes.+getNumTxData :: DatabaseReader -> PoolReads -> Word64 -> IO [TxData]+getNumTxData r pool i = do+  inPool <- pool.startingWith prefix+  chain <- matchingAsListCF r.db (txCF r.db) (TxKeyS prefix)+  let confirmed = [h | (TxKey h, td :: TxData) <- chain, not td.deleted]+  found <- known r pool (filter seventh (inPool <> confirmed))+  return (Map.elems found)+  where+    (prefix, bits) = decodeTxKey i+    seventh h = BS.index (encode h) 6 .&. 0xf8 == bits++-- | Input that spends an output, in the pool or in a block.+getSpender :: DatabaseReader -> PoolReads -> OutPoint -> IO (Maybe Spender)+getSpender r pool p =+  pool.spenders [p] >>= \case+    [Just s] -> return (Just s)+    _ -> Q.getChainSpender r p++-------------------------------------------------------------------------------+-- Addresses+-------------------------------------------------------------------------------++-- | Balance of an address, with the pool applied.+getBalance :: DatabaseReader -> PoolReads -> Address -> IO (Maybe Balance)+getBalance r pool a = M.summaryBalance a <$> Q.getBalance r a <*> (only <$> pool.summaries [a])+  where+    only = \case+      [s] -> s+      _ -> Nothing++-- | Balances of addresses, in the same order, with the pool applied.+getBalances :: DatabaseReader -> PoolReads -> [Address] -> IO [Balance]+getBalances _ _ [] = return []+getBalances r pool as = zipWith apply <$> Q.getBalances r as <*> pool.summaries as+  where+    apply c = maybe c (M.applySummary c)++-- | Unspent output, if it exists and neither a block nor the pool spends+-- it.+getUnspent :: DatabaseReader -> PoolReads -> OutPoint -> IO (Maybe Unspent)+getUnspent r pool p =+  pool.spenders [p] >>= \case+    [Just _] -> return Nothing+    _ ->+      Q.getUnspent r p >>= \case+        Just u -> return (Just u)+        Nothing -> (>>= output) . only <$> pool.bodies [p.hash]+  where+    only = \case+      [(found, _)] -> found+      _ -> Nothing+    output td = case drop (fromIntegral p.index) td.tx.outputs of+      o : _ ->+        Just+          Unspent+            { block = td.block,+              outpoint = p,+              value = o.value,+              script = o.script,+              address = eitherToMaybe (scriptToAddressBS r.net r.ctx o.script)+            }+      [] -> Nothing++-- | Where the page of a query starts, or 'Nothing' if it starts at a+-- transaction that neither the pool nor the chain has.+pageStart :: DatabaseReader -> PoolReads -> Maybe Start -> IO (Maybe From)+pageStart _ _ Nothing = return (Just Top)+pageStart _ _ (Just (AtBlock height)) = return (Just (InChain (BlockRef height maxBound)))+pageStart r pool (Just (AtTx h)) =+  known r pool [h] <&> \found -> case (.block) <$> Map.lookup h found of+    Nothing -> Nothing+    Just (MemRef time) -> Just (InMempool time h)+    Just position@BlockRef {} -> Just (InChain position)++-- | Answer a query from where its page starts.+paged :: DatabaseReader -> PoolReads -> Limits -> (From -> IO [a]) -> IO [a]+paged r pool limits answer = pageStart r pool limits.start >>= maybe (return []) answer++-- | A stream of what a function gives by the page: it is asked for a first+-- page of a size from a start, and then for more from the last item that+-- it gave, which it gives again if it still has it, for as long as it gives+-- full pages. The pages grow, so that a long list costs few reads. The+-- first is given the size of what the query is asked for, so that one read+-- is enough for a page of the API: each read of a pool that is far away+-- costs more than the rows it brings.+pages :: (Eq k) => (x -> k) -> (Maybe k -> Int -> IO [x]) -> Int -> Maybe k -> ConduitT i x IO ()+pages key fetch first = go first Nothing+  where+    go size after start = do+      rows <- lift (fetch start size)+      let fresh = maybe rows (\k -> dropWhile ((== k) . key) rows) after+      yieldMany fresh+      case reverse rows of+        final : _ | length rows == size -> go (min 4096 (size * 4)) (Just (key final)) (Just (key final))+        _ -> return ()++-- | The transactions of an address in the pool that belong in a list that+-- starts somewhere, newest first, read so many at first: none when the+-- list starts in the chain, or when the pool is known to have nothing for+-- the address.+poolTxsC :: PoolReads -> From -> Int -> Address -> Bool -> ConduitT i TxRef IO ()+poolTxsC pool from first a touched = case (from, touched) of+  (InChain _, _) -> return ()+  (_, False) -> return ()+  (Top, True) -> stream Nothing+  (InMempool time h, True) -> stream (Just (time, h))+  where+    stream start = pages id (pool.addressTxs a) first start .| mapC (\(t, h) -> TxRef {block = MemRef t, txid = h})++-- | How many items to ask a pool for at first, for a query with limits: as+-- many as the page and what is skipped before it, and one more, so that a+-- page that is not full is known to be the end; within bounds, for a query+-- without a limit.+firstPage :: Limits -> Int+firstPage limits = case limits.limit of+  0 -> 256+  n -> max 16 (min 1024 (fromIntegral (n + limits.offset) + 1))++-- | Whether a pool may have transactions of each of some addresses, for a+-- list that starts somewhere. For one address it is not asked: its+-- transactions are, which says as much in the same read. For several, the+-- summaries are read, in one read for all of them, so that only those that+-- have something are asked for it.+touchedBy :: PoolReads -> From -> [Address] -> IO [Bool]+touchedBy pool from as = case (from, as) of+  (InChain _, _) -> return (map (const False) as)+  (_, [_]) -> return [True]+  _ -> map isJust <$> pool.summaries as++-- | Transactions of any of the addresses, newest first, each once.+getAddressesTxs :: DatabaseReader -> PoolReads -> [Address] -> Limits -> IO [TxRef]+getAddressesTxs _ _ [] _ = return []+getAddressesTxs r pool as limits = paged r pool limits $ \from -> do+  touched <- touchedBy pool from as+  Q.withManyIters r (addrTxCF r.db) (map AddrTxKeyA as) $ \its ->+    Q.joinConduits txRefKey (zipWith3 (stream from) as touched its) limits+  where+    stream from a t it = poolTxsC pool from (firstPage limits) a t >> chainTxsC from a it++-- | The stream of unspent outputs of each address, from where a page+-- starts, given the iterators on the chain's index: the outputs that the+-- pool pays and does not spend, then those of the chain that the pool does+-- not spend. The pool is asked which outputs of the chain it spends+-- several at a time, four at first and then four times as many each time,+-- so that the first output of each of many addresses does not cost a batch+-- of rows read ahead, and no more once as many were found as its summary+-- counts.+unspentStreams ::+  DatabaseReader ->+  PoolReads ->+  From ->+  Limits ->+  [Address] ->+  IO ([Iterator] -> [ConduitT () Unspent IO ()])+unspentStreams r pool from limits as = do+  sums <- pool.summaries as+  return $ \its -> zipWith3 stream as sums its+  where+    stream a summary it = fresh a summary >> (chainUnspentsC r from a it .| hide a (maybe 0 hidden summary) 4)+    hidden s = s.spentCount - s.bothCount+    fresh a = \case+      Just s | s.paidCount > s.bothCount -> case from of+        Top -> outs a Nothing+        InMempool time h -> outs a (Just (time, OutPoint h 0))+        InChain _ -> return ()+      _ -> return ()+    -- some of what the pool pays it also spends, so more is asked for+    outs a start =+      pages (\(u, _) -> position u) (pool.addressOuts a) (2 * firstPage limits) start+        .| awaitForever (\(u, spent) -> if spent then return () else yield u)+    position u = case u.block of+      MemRef t -> (t, u.outpoint)+      BlockRef {} -> (0, u.outpoint)+    hide :: Address -> Word64 -> Int -> ConduitT Unspent Unspent IO ()+    hide _ 0 _ = awaitForever yield+    hide a n size = do+      batch <- gather size+      case batch of+        [] -> return ()+        _ -> do+          flags <- lift (pool.spentOutputs a (map (.outpoint) batch))+          yieldMany [u | (u, False) <- zip batch flags]+          hide a (n - min n (fromIntegral (length (filter id flags)))) (min 256 (size * 4))+    gather :: Int -> ConduitT Unspent o IO [Unspent]+    gather 0 = return []+    gather k =+      await >>= \case+        Nothing -> return []+        Just u -> (u :) <$> gather (k - 1)++-- | Unspent outputs of any of the addresses, newest first.+getAddressesUnspents :: DatabaseReader -> PoolReads -> [Address] -> Limits -> IO [Unspent]+getAddressesUnspents _ _ [] _ = return []+getAddressesUnspents r pool as limits = paged r pool limits $ \from -> do+  streams <- unspentStreams r pool from limits as+  Q.withManyIters r (addrOutCF r.db) (map AddrOutKeyA as) $ \its ->+    Q.joinConduits unspentKey (streams its) limits++-- | Unspent outputs of an extended public key, newest first, given its+-- balances.+xPubUnspents :: DatabaseReader -> PoolReads -> [XPubBal] -> Limits -> IO [XPubUnspent]+xPubUnspents r pool xbals limits = paged r pool limits $ \from -> do+  let quota = case (deOffset limits).limit of+        0 -> Nothing+        l -> Just l+      tag b = mapC (\u -> XPubUnspent {path = b.path, unspent = u})+  streams <- unspentStreams r pool from limits (map (.balance.address) funded)+  Q.withManyIters r (addrOutCF r.db) (map (AddrOutKeyA . (.balance.address)) funded) $ \its ->+    runConduit $+      mergeDescGroups+        (.unspent.block)+        (unspentKey . (.unspent))+        quota+        (zipWith (\b s -> s .| tag b) funded (streams its))+        .| applyLimitsC limits+        .| sinkList+  where+    funded = filter ((> 0) . (.balance.utxo)) xbals++-- | Number of distinct transactions of a set of addresses: those of the+-- chain, and those of the pool, read only when more than one of the+-- addresses has some there.+getAddressesTxCount :: DatabaseReader -> PoolReads -> [Address] -> IO Word32+getAddressesTxCount _ _ [] = return 0+getAddressesTxCount r pool as = do+  inBlocks <- Q.getAddressesTxCount r as+  sums <- pool.summaries as+  inPool <- case [(a, s) | (a, Just s) <- zip as sums, s.txs > 0] of+    [] -> return 0+    [(_, s)] -> return (fromIntegral s.txs)+    several -> do+      txs <- forM several $ \(a, _) -> runConduit (poolTxsC pool Top 256 a True .| mapC (.txid) .| sinkList)+      return (HashSet.size (HashSet.fromList (concat txs)))+  return (inBlocks + fromIntegral inPool)++-------------------------------------------------------------------------------+-- The store's own mempool area as a pool+-------------------------------------------------------------------------------++-- | The mempool area of a database, as a reader has it, as a pool. The+-- queries of this module with it must answer as those of+-- "Haskoin.Store.Database.Query.Layered" do, but for deleted transactions:+-- that is what the tests check, and what this is for.+localPool :: DatabaseReader -> PoolReads+localPool r =+  PoolReads+    { summaries = M.getSummaries r,+      addressTxs = \a start n -> do+        rows <- entries a+        let listed = [(t, h) | (MemAddrTxKey _ t h, _) <- rows]+            from (t, h) = dropWhile (\(t', h') -> t' > t || (t' == t && h' < h)) listed+        return (take n (maybe listed from start)),+      addressOuts = \a start n -> do+        rows <- entries a+        let spent = HashSet.fromList [p | (MemAddrSpentKey _ p, _) <- rows]+            made t p v s = Unspent {block = MemRef t, outpoint = p, value = v, script = s, address = Just a}+            listed = [(made t p v s, HashSet.member p spent) | (MemAddrOutKey _ t p, MemAddrOutVal v s) <- rows]+            place (u, _) = case u.block of+              MemRef t -> (t, encode u.outpoint)+              BlockRef {} -> (0, encode u.outpoint)+            from (t, p) = dropWhile (\x -> let (t', p') = place x in t' > t || (t' == t && p' < encode p)) listed+        return (take n (maybe listed from start)),+      spentOutputs = \a ps ->+        forM ps $ \p ->+          isJust <$> (retrieveCF r.db (mempoolAddrCF r.db) (MemAddrSpentKey a p) :: IO (Maybe MemAddrVal)),+      bodies = \hs -> case hs of+        [] -> return []+        _ -> zip <$> Q.retrieveManyCF r.db (mempoolCF r.db) (map TxKey hs) <*> mapM (M.getMempoolSpenders r) hs,+      spenders = \ps -> case ps of+        [] -> return []+        _ -> Q.retrieveManyCF r.db (mempoolCF r.db) (map MemSpenderKey ps),+      page = \offset limit -> Q.getMempoolPage r Limits {limit = limit, offset = offset, start = Nothing},+      startingWith = \prefix -> do+        rows <- matchingAsListCF r.db (mempoolCF r.db) (TxKeyS prefix)+        return [h | (TxKey h, _ :: TxData) <- rows]+    }+  where+    entries :: Address -> IO [(MemAddrKey, MemAddrVal)]+    entries a = matchingAsListCF r.db (mempoolAddrCF r.db) (MemAddrKeyA a)
src/Haskoin/Store/Database/Reader.hs view
@@ -1,13 +1,4 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-}  -- | -- Module      : Haskoin.Store.Database.Reader@@ -17,15 +8,15 @@ -- Stability   : experimental -- Portability : POSIX ----- Queries against the RocksDB database of the store.------ 'DatabaseReaderT' implements 'StoreReadBase' and 'StoreReadExtra' by reading--- the indices directly from RocksDB. The database is opened with--- 'withDatabaseReader', which also creates or migrates it.+-- The RocksDB database of the store: opening it ('withDatabaseReader'), which+-- also creates or migrates it, its column families, its metrics, and the+-- memo of the addresses of extended public keys. The queries are in+-- "Haskoin.Store.Database.Query". -- -- Data is split across column families by kind; see 'addrTxCF' and the other -- column family accessors. The keys stored in each are described in -- "Haskoin.Store.Database.Types".+-- -- = Why column families -- -- Each kind of record lives in its own column family. Their keys and access@@ -43,40 +34,34 @@ module Haskoin.Store.Database.Reader   ( -- * RocksDB Database Access     DatabaseReader (..),-    DatabaseReaderT,-    DataMetrics,-    withDB,+    DataMetrics (..),+    Derived (..),+    derivedAddrs,     createDataMetrics,     withDatabaseReader,+    withLayeredReader,+    initRocksDB,     memoryReport,     addrTxCF,     addrOutCF,     txCF,+    spenderCF,     unspentCF,     blockCF,     heightCF,     balanceCF,     mempoolCF,-    getMempoolBefore,+    mempoolAddrCF,     mempoolInsertOps,   ) where -import Conduit-import Control.Monad (forM, forM_, replicateM, (>=>))-import Control.Monad.Reader-import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import Data.Bits ((.&.))-import Data.ByteString qualified as BS+import Control.Monad (forM) import Data.ByteString.Char8 qualified as BS8 import Data.Default (def) import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap-import Data.IntMap.Strict qualified as IntMap-import Data.Functor ((<&>))-import Data.Map.Strict qualified as Map-import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe)-import Data.Serialize (Serialize, decode, encode)+import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Word (Word32)@@ -90,9 +75,6 @@ import Text.Read (readMaybe) import UnliftIO --- | Monad transformer to query the database.-type DatabaseReaderT = ReaderT DatabaseReader- -- | Open database and its settings. data DatabaseReader = DatabaseReader   { -- | RocksDB handle, with the column families of the store@@ -131,18 +113,18 @@ derivedCacheSize = 512  -- | Addresses of an xpub, from the cache or a new lazy derivation.-derivedAddrs :: (MonadIO m) => XPubSpec -> DatabaseReaderT m Derived-derivedAddrs xpub = do-  ref <- asks (.derived)-  ctx <- asks (.ctx)-  let fresh =+derivedAddrs :: DatabaseReader -> XPubSpec -> IO Derived+derivedAddrs r xpub = do+  let ref = r.derived+      ctx = r.ctx+      fresh =         let addrs m =               deriveAddresses                 (deriveFunction ctx xpub.deriv)                 (pubSubKey ctx xpub.key m)                 0          in Derived (addrs 0) (addrs 1)-  liftIO . atomicModifyIORef' ref $ \c ->+  atomicModifyIORef' ref $ \c ->     case HashMap.lookup xpub c.newer of       Just d -> (c, d)       Nothing ->@@ -200,23 +182,23 @@   where     n = 10 --- | Run an action with the metrics, if they are enabled.-withMetrics :: (MonadReader DatabaseReader m) => (DataMetrics -> m a) -> m ()-withMetrics go = asks (.metrics) >>= mapM_ go---- | Version of the database layout. Databases of versions 18 and 19 are--- migrated when opened; other versions are refused.+-- | Version of the layout that 'withDatabaseReader' opens, in which the+-- mempool is written into the indices of the chain. Databases of versions+-- 18 and 19 are migrated to it when opened that way; other versions are+-- refused. --+-- The store runs on a later one,+-- 'Haskoin.Store.Database.Migrate.currentVersion', and opens its database+-- with 'withLayeredReader' and 'Haskoin.Store.Database.Migrate.upgradeDatabase'.+-- This version is kept for the tests, which hold the new layout to the+-- answers of this one, and as the way through for databases of 18 and 19.+-- -- Bumping the version is how a change to the database layout is made safe: an -- old database is either migrated in place when it is opened or refused, never -- read with the wrong layout. dataVersion :: Word32 dataVersion = 20 --- | Run a query against a database.-withDB :: DatabaseReader -> DatabaseReaderT m a -> m a-withDB = flip runReaderT- -- | Open the database and run a query against it. Creates the database and its -- column families if missing, and migrates it from older versions. --@@ -226,7 +208,6 @@ -- families are flushed, block cache size in bytes, and metrics. 'Nothing' leaves -- a setting to RocksDB. withDatabaseReader ::-  (MonadUnliftIO m) =>   Network ->   Ctx ->   Word32 ->@@ -238,24 +219,76 @@   Maybe Int ->   Maybe Int ->   Maybe DataMetrics ->-  DatabaseReaderT m a ->-  m a-withDatabaseReader net ctx igap gap dir bloom maxOpenFiles writeBuffers maxWal blockCache stats m =-  withRunInIO $ \run ->-    withDBCF dir cfg (columnFamilyConfig bloom (isJust blockCache)) $ \db -> do-      derived <- newIORef (DerivedCache HashMap.empty HashMap.empty)-      let bdb =-            DatabaseReader-              { db = db,-                maxGap = gap,-                net = net,-                initGap = igap,-                metrics = stats,-                ctx = ctx,-                derived = derived-              }-      initRocksDB bdb-      run $ runReaderT m bdb+  (DatabaseReader -> IO a) ->+  IO a+withDatabaseReader = withReader columnFamilyConfig initRocksDB++-- | Open a database with the column families of the layered layout of+-- @docs\/layered-database.md@: those of 'withDatabaseReader' and one more,+-- for what the mempool does at each address. The arguments are those of+-- 'withDatabaseReader'.+--+-- It neither checks nor sets the version of the database:+-- 'Haskoin.Store.Database.Migrate.upgradeDatabase' does, and has to be+-- called before anything reads the database. The store opens its database+-- with the two.+--+-- Opening a database this way adds the column family to it. RocksDB only+-- opens a database when given all its column families, so a store that does+-- not know this one, such as any through 'withDatabaseReader', can no+-- longer open the database afterwards. That is the point of no return of+-- the migration.+withLayeredReader ::+  Network ->+  Ctx ->+  Word32 ->+  Word32 ->+  FilePath ->+  Bool ->+  Maybe Int ->+  Maybe Int ->+  Maybe Int ->+  Maybe Int ->+  Maybe DataMetrics ->+  (DatabaseReader -> IO a) ->+  IO a+withLayeredReader = withReader layeredFamilyConfig (\_ -> return ())++-- | Open a database with these column families, given whether to use Bloom+-- filters and whether there is a shared block cache, and run a check on it+-- before anything else. The other arguments are those of+-- 'withDatabaseReader'.+withReader ::+  (Bool -> Bool -> [(String, Config)]) ->+  (DatabaseReader -> IO ()) ->+  Network ->+  Ctx ->+  Word32 ->+  Word32 ->+  FilePath ->+  Bool ->+  Maybe Int ->+  Maybe Int ->+  Maybe Int ->+  Maybe Int ->+  Maybe DataMetrics ->+  (DatabaseReader -> IO a) ->+  IO a+withReader families check net ctx igap gap dir bloom maxOpenFiles writeBuffers maxWal blockCache stats m =+  withDBCF dir cfg (families bloom (isJust blockCache)) $ \db -> do+    derived <- newIORef (DerivedCache HashMap.empty HashMap.empty)+    let bdb =+          DatabaseReader+            { db = db,+              maxGap = gap,+              net = net,+              initGap = igap,+              metrics = stats,+              ctx = ctx,+              derived = derived+            }+    check bdb+    m bdb   where     cfg =       def@@ -284,7 +317,11 @@           <> T.justifyRight 16 ' ' (num mem)           <> T.justifyRight 16 ' ' (num readers)   return . T.unlines $-    ["** RocksDB memory (bytes) **", T.justifyLeft 10 ' ' "family" <> T.justifyRight 16 ' ' "memtables" <> T.justifyRight 16 ' ' "table readers"]+    [ "** RocksDB memory (bytes) **",+      T.justifyLeft 10 ' ' "family"+        <> T.justifyRight 16 ' ' "memtables"+        <> T.justifyRight 16 ' ' "table readers"+    ]       <> map line rows       <> [ line ("total", Just (total (\(_, m, _) -> m)), Just (total (\(_, _, r) -> r))),            "block cache: " <> num cache,@@ -302,12 +339,31 @@  -- | Column families, with Bloom filters or not, and with their index and -- filter blocks in the block cache or not.+--+-- = Why the indices by address never have filters+--+-- A filter lets RocksDB skip a table file that cannot have a key, or, for a+-- scan, a key prefix. That is safe for column families that are read by+-- key. It is not for the two that are scanned by address, as long as the+-- iterators of the binding have no bound: a scan asks RocksDB for the first+-- key at or after an address and stops when the key is of another address.+-- With filters, the files that hold the keys of the addresses that follow+-- are skipped, as they should be, but the keys that were deleted lately are+-- not in a file yet, and RocksDB steps over every one of them looking for a+-- key to give back, since it can no longer see that most still have+-- neighbours that are alive.+--+-- Measured with 5,000 mempool transactions just deleted: a scan of an+-- address that has nothing took 810 microseconds with filters on these+-- indices, and 13 without, in the layout of version 20. No filter was ever+-- built before rocksdb-haskell-jprupp 2.6.2, so it never happened to a+-- store, and filters are on by default now for everything else. columnFamilyConfig :: Bool -> Bool -> [(String, Config)] columnFamilyConfig bloom cached =-  [ ("addr-tx", c (Just 22)),-    ("addr-out", c (Just 22)),+  [ ("addr-tx", scanned (Just 22)),+    ("addr-out", scanned (Just 22)),     ("tx", c (Just 33)),-    ("spender", c (Just 33)), -- unused+    ("spender", c (Just 33)),     ("unspent", c (Just 37)),     ("block", c (Just 33)),     ("height", c Nothing),@@ -316,7 +372,41 @@   ]   where     c p = def {prefixLength = p, bloomFilter = bloom, cacheIndexAndFilterBlocks = cached}+    scanned p = (c p) {bloomFilter = False} +-- | Column families of the layered layout: those of 'columnFamilyConfig',+-- and one for the entries of the mempool by address ('MemAddrKey').+--+-- The two column families of the mempool always have Bloom filters, whatever+-- the setting for the others. Almost every lookup in them is for something+-- that is not there: a confirmed transaction, an output that no mempool+-- transaction spends, an address with nothing in the mempool. A filter+-- answers those without reading a block, and for column families this small+-- it takes a few megabytes; on the column families of the chain the same+-- filters would take gigabytes, which is why they are optional there.+--+-- The entries by address are in a column family of their own because their+-- filter is on the first 22 bytes of the key, the kind of key and the+-- address: a scan of an address with nothing in the mempool then stops at+-- the filter. The mempool column family is read by whole keys and scanned+-- across prefixes, for the index by time, so it cannot have a filter by+-- prefix.+--+-- That scan through a filter is what 'columnFamilyConfig' keeps out of the+-- indices of the chain, and it is as slow here after a block, which deletes+-- the keys of the transactions it confirms: 1.6 milliseconds for an address+-- with nothing, measured. It is one of the things that have to change before+-- this layout is used; @docs\/layered-database.md@ says how.+layeredFamilyConfig :: Bool -> Bool -> [(String, Config)]+layeredFamilyConfig bloom cached =+  map mempool (columnFamilyConfig bloom cached)+    <> [("mempool-addr", c (Just 22))]+  where+    c p = def {prefixLength = p, bloomFilter = True, cacheIndexAndFilterBlocks = cached}+    mempool (name, cfg)+      | name == "mempool" = (name, c Nothing)+      | otherwise = (name, cfg)+ -- | Column family for transactions by address ('AddrTxKey'). addrTxCF :: DB -> ColumnFamily addrTxCF db = columnFamilies db !! 0@@ -333,6 +423,11 @@ unspentCF :: DB -> ColumnFamily unspentCF db = columnFamilies db !! 4 +-- | Column family for the inputs of blocks that spend outputs+-- as t'Haskoin.Store.Database.Types.SpenderKey' says.+spenderCF :: DB -> ColumnFamily+spenderCF db = columnFamilies db !! 3+ -- | Column family for blocks ('BlockKey'). blockCF :: DB -> ColumnFamily blockCF db = columnFamilies db !! 5@@ -346,9 +441,16 @@ balanceCF db = columnFamilies db !! 7  -- | Column family for mempool transactions ('MemTxKey' and 'MemTimeKey').+-- In the layered layout it also has their bodies, under t'TxKey', and+-- their spenders, under t'MemSpenderKey'. mempoolCF :: DB -> ColumnFamily mempoolCF db = columnFamilies db !! 8 +-- | Column family for what the mempool does at each address ('MemAddrKey').+-- Only a database opened with 'withLayeredReader' has it.+mempoolAddrCF :: DB -> ColumnFamily+mempoolAddrCF db = columnFamilies db !! 9+ -- | Set the version of a new database, migrate an old one, or call 'error' if the version is not supported. initRocksDB :: DatabaseReader -> IO () initRocksDB DatabaseReader {db = db} =@@ -388,439 +490,6 @@     insertOpCF (mempoolCF db) (MemTimeKey t h) ()   ] --- | Mempool from the index by time, newest first. The index sorts last in--- the mempool column family, so seek past its prefix and walk backwards--- until a key is no longer in it.-mempoolNewestC :: Iterator -> ConduitT i (UnixTime, TxHash) IO ()-mempoolNewestC it = do-  liftIO $ do-    -- 0x0c is the first prefix after the 0x0b of 'MemTimeKey'-    iterSeek it (BS.singleton 0x0c)-    iterValid it >>= \case-      True -> iterPrev it-      False -> iterLast it-  go-  where-    go =-      liftIO (iterKey it) >>= \case-        Just k | Right (MemTimeKey t h) <- decode k -> do-          yield (t, h)-          liftIO (iterPrev it)-          go-        _ -> return ()---- | Mempool transactions that entered it before the given time, oldest--- first, at most as many as given. Reads only those entries of the index.-getMempoolBefore ::-  (MonadIO m) =>-  UnixTime ->-  Int ->-  DatabaseReaderT m [(UnixTime, TxHash)]-getMempoolBefore time n = do-  db <- asks (.db)-  liftIO . withIterCF db (mempoolCF db) $ \it ->-    runConduit $-      matching it MemTimeKeyS-        .| concatMapC entry-        .| takeWhileC ((< time) . fst)-        .| takeC n-        .| sinkList-  where-    entry (MemTimeKey t h, ()) = Just (t, h)-    entry (MemTimeKeyS, ()) = Nothing- -- | Set the version of a new database. setInitRocksDB :: DB -> IO () setInitRocksDB db = insert db VersionKey dataVersion---- | Stream the transactions of an address, newest first, from a starting point.--- If the start is a transaction that is not found, nothing is streamed.-addressConduit ::-  (MonadIO m) =>-  Address ->-  Maybe Start ->-  Iterator ->-  ConduitT i TxRef (DatabaseReaderT m) ()-addressConduit a s it = do-  pipe .| mapC (uncurry f)-  where-    f (AddrTxKey _ t) () = t-    f _ _ = undefined-    pipe = case s of-      Nothing ->-        readerC $ \_ ->-          transPipe liftIO $-            matching it (AddrTxKeyA a)-      Just (AtBlock bh) ->-        readerC $ \_ ->-          transPipe liftIO $-            matchingSkip-              it-              (AddrTxKeyA a)-              (AddrTxKeyB a (BlockRef bh maxBound))-      Just (AtTx txh) ->-        lift (getTxData txh) >>= \case-          Just TxData {block = b@BlockRef {}} ->-            readerC $ \_ ->-              transPipe liftIO $-                matchingSkip-                  it-                  (AddrTxKeyA a)-                  (AddrTxKeyB a b)-          Just TxData {block = MemRef {}} ->-            let cond (AddrTxKey _ (TxRef MemRef {} th)) = th /= txh-                cond (AddrTxKey _ (TxRef BlockRef {} _)) = False-                cond _ = undefined-             in readerC $ \_ ->-                  transPipe liftIO $-                    matching it (AddrTxKeyA a)-                      .| (dropWhileC (cond . fst) >> mapC id)-          Nothing -> return ()---- | Stream the unspent outputs of an address, newest first, from a starting--- point. If the start is a transaction that is not found, nothing is streamed.-unspentConduit ::-  (MonadIO m) =>-  Network ->-  Ctx ->-  Address ->-  Maybe Start ->-  Iterator ->-  ConduitT i Unspent (DatabaseReaderT m) ()-unspentConduit net ctx a s it =-  x .| mapC (uncurry (toUnspent net ctx)) .| concatC-  where-    x = case s of-      Nothing ->-        readerC $ \_ -> transPipe liftIO $ matching it (AddrOutKeyA a)-      Just (AtBlock h) ->-        readerC $ \_ ->-          transPipe liftIO $-            matchingSkip-              it-              (AddrOutKeyA a)-              (AddrOutKeyB a (BlockRef h maxBound))-      Just (AtTx txh) ->-        lift (getTxData txh) >>= \case-          Just TxData {block = b@BlockRef {}} ->-            readerC $ \_ ->-              transPipe liftIO $-                matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)-          Just TxData {block = MemRef {}} ->-            let cond (AddrOutKey _ MemRef {} p) = p.hash /= txh-                cond (AddrOutKey _ BlockRef {} _) = False-                cond _ = undefined-             in readerC $ \_ ->-                  transPipe liftIO $-                    matching it (AddrOutKeyA a)-                      .| (dropWhileC (cond . fst) >> mapC id)-          Nothing -> return ()---- | Run an action with an iterator on a column family, counting it in the metrics.-withIterCFStat ::-  (MonadUnliftIO m) =>-  DB ->-  ColumnFamily ->-  (Iterator -> DatabaseReaderT m a) ->-  DatabaseReaderT m a-withIterCFStat db cf f = do-  m <- asks (.metrics)-  bracket_ (open m) (close m) go-  where-    open (Just m) = incrementGauge m.dataIters 1-    open Nothing = return ()-    close (Just m) = decrementGauge m.dataIters 1-    close Nothing = return ()-    go = withRunInIO $ \run ->-      withIterCF db cf $ \it -> run (f it)---- | Run an action with this many iterators on a column family. They are--- opened and closed in a single bracket rather than one bracket each: each--- bracket leaves a frame on the stack, and every call into RocksDB through--- an iterator walks the stack, so with one bracket per address an xpub of a--- hundred addresses made each step of each iterator a hundred times slower.-withManyIters ::-  (MonadUnliftIO m) =>-  DB ->-  ColumnFamily ->-  Int ->-  ([Iterator] -> DatabaseReaderT m a) ->-  DatabaseReaderT m a-withManyIters db cf i f = do-  m <- asks (.metrics)-  bracket_ (gauge m i) (gauge m (negate i)) $-    withRunInIO $ \run ->-      bracket (newIORef []) (readIORef >=> mapM_ destroyIterator) $ \ref -> do-        its <- replicateM i . mask_ $ do-          it <- createIterator db (Just cf)-          modifyIORef' ref (it :)-          return it-        run (f its)-  where-    gauge (Just m) n = incrementGauge m.dataIters n-    gauge Nothing _ = return ()---- | Values of many keys of a column family, read with one multi-get, in--- the order of the keys.-retrieveManyCF ::-  (KeyValue key value, Serialize key, Serialize value) =>-  DB ->-  ColumnFamily ->-  [key] ->-  IO [Maybe value]-retrieveManyCF db cf keys =-  map (fmap (either error id . decode)) <$> multiGetCF db cf (map encode keys)---- | Merge streams sorted newest first, drop duplicates, and apply the offset and--- limit.-joinConduits ::-  (Monad m, Ord o) =>-  [ConduitT () o m ()] ->-  Limits ->-  m [o]-joinConduits cs l =-  runConduit $ joinDescStreams cs .| applyLimitsC l .| sinkList--instance (MonadIO m) => StoreReadBase (DatabaseReaderT m) where-  getCtx = asks (.ctx)-  getNetwork = asks (.net)--  getTxData th = do-    db <- asks (.db)-    liftIO (retrieveCF db (txCF db) (TxKey th)) >>= \case-      Nothing -> return Nothing-      Just t -> do-        withMetrics $ \s -> incrementCounter s.dataTxCount 1-        return (Just t)--  getManyTxData ths = do-    db <- asks (.db)-    tds <- liftIO (retrieveManyCF db (txCF db) (map TxKey ths))-    withMetrics $ \s -> incrementCounter s.dataTxCount (length (catMaybes tds))-    return tds--  getSpender op = runMaybeT $ do-    td <- MaybeT $ getTxData op.hash-    let i = fromIntegral op.index-    MaybeT . return $ i `IntMap.lookup` td.spenders--  getUnspent p = do-    db <- asks (.db)-    net <- asks (.net)-    ctx <- asks (.ctx)-    val <- liftIO (retrieveCF db (unspentCF db) (UnspentKey p))-    case fmap (valToUnspent net ctx p) val of-      Nothing -> return Nothing-      Just u -> do-        withMetrics $ \s -> incrementCounter s.dataUnspentCount 1-        return (Just u)--  getBalance a = do-    db <- asks (.db)-    withMetrics $ \s -> incrementCounter s.dataBalanceCount 1-    fmap (valToBalance a) <$> liftIO (retrieveCF db (balanceCF db) (BalKey a))--  getMempool = getMempoolPage def--  getMempoolPage limits = do-    db <- asks (.db)-    withMetrics $ \s -> incrementCounter s.dataMempoolCount 1-    liftIO . withIterCF db (mempoolCF db) $ \it ->-      runConduit $-        mempoolNewestC it-          .| applyLimitsC limits-          .| sinkList--  getBestBlock = do-    withMetrics $ \s -> incrementCounter s.dataBestCount 1-    asks (.db) >>= liftIO . (`retrieve` BestKey)--  getBlocksAtHeight h = do-    db <- asks (.db)-    liftIO (retrieveCF db (heightCF db) (HeightKey h)) >>= \case-      Nothing -> return []-      Just ls -> do-        withMetrics $ \s -> incrementCounter s.dataBlockCount (length ls)-        return ls--  getBlock h = do-    db <- asks (.db)-    liftIO (retrieveCF db (blockCF db) (BlockKey h)) >>= \case-      Nothing -> return Nothing-      Just b -> do-        withMetrics $ \s -> incrementCounter s.dataBlockCount 1-        return (Just b)--instance (MonadUnliftIO m) => StoreReadExtra (DatabaseReaderT m) where-  getAddressesTxs addrs limits = do-    db <- asks (.db)-    withManyIters db (addrTxCF db) (length addrs) $ \its -> do-      txs <- joinConduits (cs its) limits-      withMetrics $ \s -> incrementCounter s.dataAddrTxCount (length txs)-      return txs-    where-      cs = zipWith c addrs-      c a = addressConduit a limits.start--  getAddressesUnspents addrs limits = do-    db <- asks (.db)-    net <- asks (.net)-    ctx <- asks (.ctx)-    withManyIters db (addrOutCF db) (length addrs) $ \its -> do-      uns <- joinConduits (cs net ctx its) limits-      withMetrics $ \s -> incrementCounter s.dataUnspentCount (length uns)-      return uns-    where-      cs net ctx = zipWith (c net ctx) addrs-      c net ctx a = unspentConduit net ctx a limits.start--  getAddressUnspents a limits = do-    db <- asks (.db)-    net <- asks (.net)-    ctx <- asks (.ctx)-    us <- withIterCFStat db (addrOutCF db) $ \it ->-      runConduit $-        unspentConduit net ctx a limits.start it-          .| applyLimitsC limits-          .| sinkList-    withMetrics $ \s -> incrementCounter s.dataUnspentCount (length us)-    return us--  getAddressTxs a limits = do-    db <- asks (.db)-    txs <- withIterCFStat db (addrTxCF db) $ \it ->-      runConduit $-        addressConduit a limits.start it-          .| applyLimitsC limits-          .| sinkList-    withMetrics $ \s -> incrementCounter s.dataAddrTxCount (length txs)-    return txs--  getMaxGap = asks (.maxGap)--  getInitialGap = asks (.initGap)--  getNumTxData i = ReaderT $ \r -> do-    let (sk, w) = decodeTxKey i-    ls <- liftIO $ matchingAsListCF r.db (txCF r.db) (TxKeyS sk)-    let f t =-          let bs = encode $ txHash t.tx-              b = BS.head (BS.drop 6 bs)-              w' = b .&. 0xf8-           in w == w'-        txs = filter f $ map snd ls-    forM_ r.metrics $ \s -> incrementCounter s.dataTxCount (length txs)-    return txs--  getBalances [] = return []-  getBalances as = do-    db <- asks (.db)-    withMetrics $ \s -> incrementCounter s.dataBalanceCount (length as)-    vals <- liftIO (retrieveManyCF db (balanceCF db) (map BalKey as))-    return (zipWith f as vals)-    where-      f a Nothing = zeroBalance a-      f a (Just v) = valToBalance a v--  xPubBals xpub = do-    igap <- asks (.initGap)-    gap <- asks (.maxGap)-    Derived ext chg <- derivedAddrs xpub-    ext1 <- derive_until_gap gap 0 (take (fromIntegral igap) ext)-    if all nullxb ext1-      then do-        withMetrics $ \s -> incrementCounter s.dataXPubBals (length ext1)-        return ext1-      else do-        ext2 <- derive_until_gap gap 0 (drop (fromIntegral igap) ext)-        chg' <- derive_until_gap gap 1 chg-        let bals = ext1 <> ext2 <> chg'-        withMetrics $ \s -> incrementCounter s.dataXPubBals (length bals)-        return bals-    where-      nullxb b = nullBalance b.balance-      derive_until_gap gap m adrs =-        let xb b n = XPubBal {path = [m, n], balance = b}-            ig = fromIntegral gap-            test acc =-              let ns = takeWhile nullxb acc-               in length ns > ig-            go acc [] = return $ reverse acc-            go acc as-              | test acc =-                  let (ns, ys) = span nullxb acc-                      xs = take ig $ reverse ns-                   in return $ reverse ys <> xs-              | otherwise = do-                  let (as', as'') = splitAt ig as-                  bs <- getBalances (map snd as')-                  let xs = reverse $ zipWith xb bs (map fst as')-                  go (xs <> acc) as''-         in go [] adrs--  -- The first unspent outputs of each address, at most offset plus limit-  -- of them, sorted together. Read the addresses in step, so that each is-  -- read only as far as the page needs, rather than decoding the whole-  -- quota of every address to keep one page of it.-  xPubUnspents _xspec xbals limits = do-    db <- asks (.db)-    net <- asks (.net)-    ctx <- asks (.ctx)-    let quota = case (deOffset limits).limit of-          0 -> Nothing-          l -> Just l-        stream b it =-          unspentConduit net ctx b.balance.address limits.start it-            .| mapC (\u -> XPubUnspent {path = b.path, unspent = u})-    us <- withManyIters db (addrOutCF db) (length cs) $ \its ->-      runConduit $-        mergeDescGroups (.unspent.block) quota (zipWith stream cs its)-          .| applyLimitsC limits-          .| sinkList-    withMetrics $ \s -> incrementCounter s.dataXPubUnspents (length us)-    return us-    where-      cs = filter ((> 0) . (.balance.utxo)) xbals--  xPubTxs _xspec xbals limits = do-    let as =-          map (.address) $-            filter (not . nullBalance) $-              map (.balance) xbals-    txs <- getAddressesTxs as limits-    withMetrics $ \s -> incrementCounter s.dataXPubTxs (length txs)-    return txs--  -- Count the transactions of all the addresses, each once even when it-  -- touches several of them. The entries of an address are sorted, and the-  -- same transaction has the same key after the address prefix in every-  -- address, so merge the addresses by those raw key bytes and count the-  -- distinct ones, without decoding any key or reading any value.-  xPubTxCount _xspec xbals = do-    withMetrics $ \s -> incrementCounter s.dataXPubTxCount 1-    db <- asks (.db)-    let as =-          map (.address) $-            filter (not . nullBalance) $-              map (.balance) xbals-    withManyIters db (addrTxCF db) (length as) $ \its ->-      liftIO $ fromIntegral <$> countDistinctSuffixes (zip (map (encode . AddrTxKeyA) as) its)---- | Number of distinct keys, after their prefix, in a set of iterators that--- each walk the keys with one prefix, in order.-countDistinctSuffixes :: [(BS.ByteString, Iterator)] -> IO Int-countDistinctSuffixes streams = do-  heads <- mapM start streams-  go 0 (Map.fromListWith (<>) [(k, [x]) | (k, x) <- catMaybes heads])-  where-    start x@(pfx, it) = iterSeek it pfx >> current x-    current x@(pfx, it) =-      iterKey it <&> \case-        Just k | pfx `BS.isPrefixOf` k -> Just (BS.drop (BS.length pfx) k, x)-        _ -> Nothing-    advance x@(_, it) = iterNext it >> current x-    go !n m = case Map.minViewWithKey m of-      Nothing -> return n-      Just ((_, xs), m') -> do-        nexts <- catMaybes <$> mapM advance xs-        go (n + 1) (foldr (\(k, x) -> Map.insertWith (<>) k [x]) m' nexts)
src/Haskoin/Store/Database/Types.hs view
@@ -1,56 +1,77 @@ {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-} -{- |-Module      : Haskoin.Store.Database.Types-Copyright   : (c) Haskoin Developers-License     : MIT-Maintainer  : jprupp@protonmail.ch-Stability   : experimental-Portability : POSIX--Keys and values stored in RocksDB.--Every key type serializes to a distinct first byte, so that keys of different-types never collide even when they share a column family. Many key types-have short variants (suffixed @A@, @B@ or @S@) that serialize to a prefix of-the full key, for use with the prefix queries of "Database.RocksDB.Query".--+-----------+--------------+----------------------------------------+-| Prefix    | Key          | Value                                  |-+===========+==============+========================================+-| 32 x 0x00 | 'BestKey'    | hash of the best block                 |-+-----------+--------------+----------------------------------------+-| 0x01      | 'BlockKey'   | 'BlockData'                            |-+-----------+--------------+----------------------------------------+-| 0x02      | 'TxKey'      | 'TxData'                               |-+-----------+--------------+----------------------------------------+-| 0x03      | 'HeightKey'  | hashes of the blocks at a height       |-+-----------+--------------+----------------------------------------+-| 0x04      | 'BalKey'     | 'BalVal'                               |-+-----------+--------------+----------------------------------------+-| 0x05      | 'AddrTxKey'  | nothing                                |-+-----------+--------------+----------------------------------------+-| 0x06      | 'AddrOutKey' | 'OutVal'                               |-+-----------+--------------+----------------------------------------+-| 0x07      | 'MemKey'     | whole mempool (legacy)                 |-+-----------+--------------+----------------------------------------+-| 0x08      | 'MemTxKey'   | time a transaction entered the mempool |-+-----------+--------------+----------------------------------------+-| 0x09      | 'UnspentKey' | 'UnspentVal'                           |-+-----------+--------------+----------------------------------------+-| 0x0a      | 'VersionKey' | database version                       |-+-----------+--------------+----------------------------------------+-| 0x0b      | 'MemTimeKey' | nothing                                |-+-----------+--------------+----------------------------------------+--}+-- |+-- Module      : Haskoin.Store.Database.Types+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Keys and values stored in RocksDB.+--+-- Every key type serializes to a distinct first byte, so that keys of different+-- types never collide even when they share a column family. Many key types+-- have short variants (suffixed @A@, @B@ or @S@) that serialize to a prefix of+-- the full key, for use with the prefix queries of "Database.RocksDB.Query".+--+-- +-----------+--------------+----------------------------------------++-- | Prefix    | Key          | Value                                  |+-- +===========+==============+========================================++-- | 32 x 0x00 | 'BestKey'    | hash of the best block                 |+-- +-----------+--------------+----------------------------------------++-- | 0x01      | 'BlockKey'   | 'BlockData'                            |+-- +-----------+--------------+----------------------------------------++-- | 0x02      | 'TxKey'      | 'TxData'                               |+-- +-----------+--------------+----------------------------------------++-- | 0x03      | 'HeightKey'  | hashes of the blocks at a height       |+-- +-----------+--------------+----------------------------------------++-- | 0x04      | 'BalKey'     | 'BalVal'                               |+-- +-----------+--------------+----------------------------------------++-- | 0x05      | 'AddrTxKey'  | nothing                                |+-- +-----------+--------------+----------------------------------------++-- | 0x06      | 'AddrOutKey' | 'OutVal'                               |+-- +-----------+--------------+----------------------------------------++-- | 0x07      | 'MemKey'     | whole mempool (legacy)                 |+-- +-----------+--------------+----------------------------------------++-- | 0x08      | 'MemTxKey'   | time a transaction entered the mempool |+-- +-----------+--------------+----------------------------------------++-- | 0x09      | 'UnspentKey' | 'UnspentVal'                           |+-- +-----------+--------------+----------------------------------------++-- | 0x0a      | 'VersionKey' | database version                       |+-- +-----------+--------------+----------------------------------------++-- | 0x0b      | 'MemTimeKey' | nothing                                |+-- +-----------+--------------+----------------------------------------++--+-- = Keys of the layered layout+--+-- They belong to the layout that the store runs on, that of+-- @docs\/layered-database.md@, version 22, in which the column families of+-- the chain hold nothing from the mempool and everything about the mempool+-- is in two column families of its own. t'TxKey' is used there too, for the+-- bodies of mempool transactions. "Haskoin.Store.Database.Mempool" reads+-- and writes them, and "Haskoin.Store.Database.Migrate" moves a database of+-- version 20 to that layout.+--+-- +-----------+------------------+-------------------------------------------++-- | Prefix    | Key              | Value                                     |+-- +===========+==================+===========================================++-- | 0x0c      | t'MemSpenderKey' | 'Spender': mempool input that spends an   |+-- |           |                  | output                                    |+-- +-----------+------------------+-------------------------------------------++-- | 0x0d      | 'MemAddrKey'     | 'MemAddrVal': what the mempool does at an |+-- |           |                  | address                                   |+-- +-----------+------------------+-------------------------------------------++-- | 0x0e      | t'MemDeletedKey' | 'TxData' of a transaction that left the   |+-- |           |                  | mempool unconfirmed: not written any more |+-- +-----------+------------------+-------------------------------------------++-- | 0x0f      | t'MigrationKey'  | version a migration in progress started   |+-- |           |                  | from                                      |+-- +-----------+------------------+-------------------------------------------++-- | 0x10      | t'SpenderKey'    | t'SpenderVal': inputs of blocks that      |+-- |           |                  | spend outputs of a transaction            |+-- +-----------+------------------+-------------------------------------------+ module Haskoin.Store.Database.Types   ( AddrTxKey (..),     AddrOutKey (..),@@ -61,6 +82,16 @@     MemKey (..),     MemTxKey (..),     MemTimeKey (..),+    MemSpenderKey (..),+    MemAddrKey (..),+    MemAddrVal (..),+    MemSummary (..),+    MemDeletedKey (..),+    MigrationKey (..),+    SpenderKey (..),+    SpenderVal (..),+    spenderGroup,+    spenderKey,     TxKey (..),     decodeTxKey,     UnspentKey (..),@@ -77,7 +108,7 @@ where  import Control.DeepSeq (NFData)-import Control.Monad (guard)+import Control.Monad (forM_, guard, replicateM) import Data.Bits   ( shift,     (.&.),@@ -86,8 +117,11 @@ import Data.ByteString qualified as BS import Data.Default (Default (..)) import Data.Hashable (Hashable)+import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IntMap import Data.Serialize-  ( Serialize (..),+  ( Get,+    Serialize (..),     getBytes,     getWord16be,     getWord32be,@@ -116,6 +150,7 @@   ( Balance (..),     BlockData,     BlockRef,+    Spender,     TxData,     TxRef (..),     UnixTime,@@ -337,7 +372,7 @@  -- | Index of the mempool by the time each transaction entered it, in the -- mempool column family next to 'MemTxKey'. Keys sort by time, then by--- hash, as the pairs in 'getMempool' do, so a page of the mempool is read+-- hash, as the pairs in @getMempool@ do, so a page of the mempool is read -- with a short scan instead of loading and sorting every entry. data MemTimeKey   = -- | entry of a mempool transaction, by time and hash@@ -362,9 +397,330 @@  instance KeyValue MemTimeKey () +-- | Key of the input of a mempool transaction that spends an output, in the+-- mempool column family of the layered layout. The output may be one of a+-- confirmed transaction or of another mempool transaction: the key is the+-- same, so nothing has to change here when the transaction that created the+-- output is confirmed.+--+-- Spenders in the chain's column families are part of the body of the+-- transaction whose output they spend. A mempool spender cannot be: writing+-- it there would let the mempool change the data of the chain.+data MemSpenderKey+  = -- | key of the mempool spender of an output+    MemSpenderKey !OutPoint+  | -- | short key that matches the mempool spenders of the outputs of a+    -- transaction+    MemSpenderKeyS !TxHash+  | -- | short key that matches every mempool spender+    MemSpenderKeyB+  deriving (Show, Read, Eq, Ord, Generic, Hashable)++instance Serialize MemSpenderKey where+  -- 0x0c · TxHash · Index+  put (MemSpenderKey OutPoint {hash = h, index = i}) = do+    put (MemSpenderKeyS h)+    put i+  -- 0x0c · TxHash+  put (MemSpenderKeyS h) = do+    put MemSpenderKeyB+    put h+  -- 0x0c+  put MemSpenderKeyB = putWord8 0x0c+  get = do+    guard . (== 0x0c) =<< getWord8+    h <- get+    i <- get+    return $ MemSpenderKey OutPoint {hash = h, index = i}++instance Key MemSpenderKey++instance KeyValue MemSpenderKey Spender++-- | Key of something that the mempool does at an address, in the column+-- family of mempool address records of the layered layout. There are three+-- kinds, and all those of an address are read with one scan of its prefix.+--+-- Every entry depends on one mempool transaction alone: on its inputs, with+-- the outputs they spend, and on its outputs. None says whether an output+-- that is spent was created by a confirmed transaction or by another+-- mempool transaction, nor whether an output that is created is spent. So+-- a transaction is added by writing its entries and removed by deleting+-- them, whatever else is in the mempool, and nothing here changes when a+-- block confirms some transactions and not others. What a query needs is+-- worked out from the entries of the address when it is answered; see+-- "Haskoin.Store.Database.Mempool".+--+-- Times are stored complemented, as in a mempool 'BlockRef', so that entries+-- come out newest first, which is the order of the API.+data MemAddrKey+  = -- | a mempool transaction that spends from or pays to the address, by+    -- the time it entered the mempool+    MemAddrTxKey !Address !UnixTime !TxHash+  | -- | an output of the address that a mempool transaction spends+    MemAddrSpentKey !Address !OutPoint+  | -- | an output that a mempool transaction pays to the address, spent or+    -- not, by the time the transaction entered the mempool+    MemAddrOutKey !Address !UnixTime !OutPoint+  | -- | what the entries of an address add up to; see t'MemSummary'+    MemAddrSumKey !Address+  | -- | short key that matches the transactions of an address+    MemAddrTxKeyA !Address+  | -- | short key that matches the outputs paid to an address+    MemAddrOutKeyA !Address+  | -- | short key that matches every entry of an address+    MemAddrKeyA !Address+  | -- | short key that matches the entries of every address+    MemAddrKeyS+  deriving (Show, Eq, Ord, Generic, Hashable)++instance Serialize MemAddrKey where+  -- 0x0d · Address · 0x00 · time · TxHash+  put (MemAddrTxKey a t h) = do+    put (MemAddrKeyA a)+    putWord8 0x00+    putWord64be (maxBound - t)+    put h+  -- 0x0d · Address · 0x01 · OutPoint+  put (MemAddrSpentKey a p) = do+    put (MemAddrKeyA a)+    putWord8 0x01+    put p+  -- 0x0d · Address · 0x02 · time · OutPoint+  put (MemAddrOutKey a t p) = do+    put (MemAddrKeyA a)+    putWord8 0x02+    putWord64be (maxBound - t)+    put p+  -- 0x0d · Address · 0x03+  put (MemAddrSumKey a) = do+    put (MemAddrKeyA a)+    putWord8 0x03+  -- 0x0d · Address · 0x00+  put (MemAddrTxKeyA a) = do+    put (MemAddrKeyA a)+    putWord8 0x00+  -- 0x0d · Address · 0x02+  put (MemAddrOutKeyA a) = do+    put (MemAddrKeyA a)+    putWord8 0x02+  -- 0x0d · Address+  put (MemAddrKeyA a) = do+    put MemAddrKeyS+    put a+  -- 0x0d+  put MemAddrKeyS = putWord8 0x0d+  get = do+    guard . (== 0x0d) =<< getWord8+    a <- get+    getWord8 >>= \case+      0x00 -> MemAddrTxKey a . (maxBound -) <$> getWord64be <*> get+      0x01 -> MemAddrSpentKey a <$> get+      0x02 -> MemAddrOutKey a . (maxBound -) <$> getWord64be <*> get+      0x03 -> return (MemAddrSumKey a)+      _ -> fail "Cannot decode MemAddrKey"++instance Key MemAddrKey++-- | Value of a 'MemAddrKey': nothing for a transaction, the amount in+-- satoshi of an output that is spent, and the amount and script of an output+-- that is created. The first byte says which, so that a value can be decoded+-- without its key.+data MemAddrVal+  = -- | value of a 'MemAddrTxKey'+    MemAddrTxVal+  | -- | value of a 'MemAddrSpentKey': amount of the output+    MemAddrSpentVal !Word64+  | -- | value of a 'MemAddrOutKey': amount and script of the output+    MemAddrOutVal !Word64 !ByteString+  | -- | value of a 'MemAddrSumKey'+    MemAddrSumVal !MemSummary+  deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)++-- | What the entries of an address in the mempool area add up to, kept so+-- that its balance is one read by key however many entries there are, and+-- so that an address with nothing in the mempool is found by a key that is+-- not there instead of a scan.+--+-- It is a function of the entries of the address, and of nothing else: not+-- of the chain. Whether an output that the mempool spends is one of the+-- chain or one that the mempool made is the difference between 'spent' and+-- 'both', and 'both' only changes when one of the two transactions enters+-- or leaves the mempool. So the summary stays right when a block confirms+-- some transactions and not others, like the entries themselves.+data MemSummary = MemSummary+  { -- | mempool transactions that touch the address+    txs :: !Word64,+    -- | amount of the outputs that the mempool pays to the address+    paid :: !Word64,+    -- | number of those outputs+    paidCount :: !Word64,+    -- | amount of the outputs of the address that the mempool spends+    spent :: !Word64,+    -- | number of those outputs+    spentCount :: !Word64,+    -- | amount of the outputs that the mempool both pays and spends+    both :: !Word64,+    -- | number of those outputs+    bothCount :: !Word64+  }+  deriving (Show, Read, Eq, Ord, Generic, Hashable, Serialize, NFData)++instance Serialize MemAddrVal where+  put MemAddrTxVal = putWord8 0x00+  put (MemAddrSpentVal v) = putWord8 0x01 >> putWord64be v+  put (MemAddrOutVal v s) = putWord8 0x02 >> putWord64be v >> put s+  put (MemAddrSumVal s) = putWord8 0x03 >> put s+  get =+    getWord8 >>= \case+      0x00 -> return MemAddrTxVal+      0x01 -> MemAddrSpentVal <$> getWord64be+      0x02 -> MemAddrOutVal <$> getWord64be <*> get+      0x03 -> MemAddrSumVal <$> get+      _ -> fail "Cannot decode MemAddrVal"++instance KeyValue MemAddrKey MemAddrVal++-- | Key of the body of a transaction that left the mempool without being+-- confirmed, in the mempool column family of the layered layout: it was+-- replaced, it conflicted with a block, or it was too old. The store keeps+-- such bodies, marked as deleted, so that the API can go on saying what+-- became of a transaction that it once showed.+--+-- Version 20 keeps them in the column family of transactions, under+-- t'TxKey'. There, deleting a mempool transaction writes to the data of the+-- chain, which the layered layout does not allow.+--+-- Nothing writes or reads this key any more: since version 22 the store+-- keeps nothing of a transaction that leaves the mempool unconfirmed. A+-- database that has some has them until its mempool is wiped.+data MemDeletedKey+  = -- | key of a deleted transaction+    MemDeletedKey !TxHash+  | -- | short key that matches the deleted transactions whose hash starts+    -- with these six bytes, as 'TxKeyS' does for the others; see+    -- 'decodeTxKey'+    MemDeletedKeyP !(Word32, Word16)+  | -- | short key that matches every deleted transaction+    MemDeletedKeyS+  deriving (Show, Read, Eq, Ord, Generic, Hashable)++instance Serialize MemDeletedKey where+  -- 0x0e · TxHash+  put (MemDeletedKey h) = do+    put MemDeletedKeyS+    put h+  -- 0x0e · first six bytes of a TxHash+  put (MemDeletedKeyP h) = do+    put MemDeletedKeyS+    put h+  -- 0x0e+  put MemDeletedKeyS = putWord8 0x0e+  get = do+    guard . (== 0x0e) =<< getWord8+    MemDeletedKey <$> get++instance Key MemDeletedKey++instance KeyValue MemDeletedKey TxData++-- | Key that is present while a migration of the database is under way,+-- with the version that it started from. A migration that works in several+-- batches sets it with the first and removes it with the last, so that a+-- store that finds it knows to carry on, whatever the version says.+data MigrationKey = MigrationKey+  deriving (Eq, Show, Read, Ord, Generic, Hashable)++instance Serialize MigrationKey where+  -- 0x0f+  put MigrationKey = putWord8 0x0f+  get = do+    guard . (== 0x0f) =<< getWord8+    return MigrationKey++instance Key MigrationKey++instance KeyValue MigrationKey Word32++-- | Key of the inputs of blocks that spend some outputs of a transaction,+-- in the @spender@ column family, since database version 22: the+-- transaction, and the number of a group of 'spenderGroup' of its outputs+-- that follow each other. The value has the spender of each output of the+-- group that is spent in a t'SpenderVal'.+--+-- Until then the spenders of the outputs of a confirmed transaction were in+-- its own record, 'TxData', which was read and written whole again every+-- time a block spent one of them or was reverted: once for each of its+-- outputs, over years, for a record of any size. Now the record of a+-- transaction is written when a block confirms it and never again, and+-- spending an output reads and writes a value of a few dozen bytes.+--+-- = Why groups+--+-- A key for each spent output would make a spend one write that reads+-- nothing. But a transaction is read far more often than it is spent+-- from, and has to be read with its spenders: that would take a key for+-- each of its outputs, with each read of it. One value+-- for all the outputs of a transaction would cost one key to read, and+-- bring back the trouble with large transactions: a payout to ten+-- thousand would rewrite half a megabyte ten thousand times. With groups+-- of sixteen nearly every transaction has one key, and no value is larger+-- than sixteen spenders.+--+-- The groups of a transaction are next to each other, so all its spenders+-- can also be read with one scan of their common prefix, which is what the+-- prefix of the column family is.+data SpenderKey+  = -- | spenders of a group of outputs of a transaction+    SpenderKey !TxHash !Word32+  | -- | short key that matches the groups of a transaction+    SpenderKeyS !TxHash+  deriving (Show, Read, Eq, Ord, Generic, Hashable)++instance Serialize SpenderKey where+  -- 0x10 · TxHash · group+  put (SpenderKey h g) = do+    put (SpenderKeyS h)+    put g+  -- 0x10 · TxHash+  put (SpenderKeyS h) = do+    putWord8 0x10+    put h+  get = do+    guard . (== 0x10) =<< getWord8+    h <- get+    SpenderKey h <$> get++instance Key SpenderKey++instance KeyValue SpenderKey SpenderVal++-- | The number of outputs whose spenders are under one t'SpenderKey'.+spenderGroup :: Word32+spenderGroup = 16++-- | The key that has the spender of an output.+spenderKey :: OutPoint -> SpenderKey+spenderKey p = SpenderKey p.hash (p.index `div` spenderGroup)++-- | The inputs of blocks that spend outputs of a group, by the number of+-- the output in its transaction. A group none of whose outputs is spent+-- has no key.+newtype SpenderVal = SpenderVal (IntMap Spender)+  deriving (Show, Eq)++instance Serialize SpenderVal where+  put (SpenderVal m) = do+    putWord8 (fromIntegral (IntMap.size m))+    forM_ (IntMap.toAscList m) $ \(i, spender) -> put (fromIntegral i :: Word32) >> put spender+  get = do+    n <- getWord8+    SpenderVal . IntMap.fromList <$> replicateM (fromIntegral n) ((,) . fromIntegral <$> (get :: Get Word32) <*> get)+ -- | Block entry database key. newtype BlockKey = BlockKey BlockHash-  deriving (Show, Read, Eq, Ord, Generic, Hashable)+  deriving stock (Show, Read, Eq, Ord, Generic)+  deriving anyclass (Hashable)  instance Serialize BlockKey where   -- 0x01 · BlockHash@@ -381,7 +737,8 @@  -- | Block height database key. newtype HeightKey = HeightKey BlockHeight-  deriving (Show, Read, Eq, Ord, Generic, Hashable)+  deriving stock (Show, Read, Eq, Ord, Generic)+  deriving anyclass (Hashable)  instance Serialize HeightKey where   -- 0x03 · BlockHeight
+ src/Haskoin/Store/Database/Views.hs view
@@ -0,0 +1,136 @@+-- |+-- Module      : Haskoin.Store.Database.Views+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The database as it was at each of its recent blocks: a RocksDB snapshot+-- for each, kept until it is no longer needed. Nothing in the store uses+-- this yet. It is the first part of the store's side of "A fleet that+-- answers as one" in @docs\/layered-database.md@.+--+-- = Why+--+-- Stores that share a Redis all answer from one block, the fleet tip+-- ("Haskoin.Store.Fleet"), which is usually not the newest block that a+-- store has: the tip waits for the slowest store. So a store has to be able+-- to answer from a block that it has already built on, and a snapshot is how+-- RocksDB gives the database as it was. A snapshot cannot be taken of the+-- past, only of the present, so the store takes one as each block is+-- imported and keeps it.+--+-- = Letting go+--+-- RocksDB holds on to every version of a key that a snapshot can see, so+-- snapshots are let go as soon as the rules of the fleet allow ('keepViews').+-- A request may be reading through one at that moment. Releasing it then+-- would hand RocksDB freed memory, so each view counts its readers, and one+-- that is let go while it is read is released when the last reader is done.+--+-- The blocks can be of any type that can be compared, as in+-- "Haskoin.Store.Fleet".+module Haskoin.Store.Database.Views+  ( Views,+    newViews,+    closeViews,+    takeView,+    heldViews,+    keepViews,+    withViewAt,+  )+where++import Control.Monad (forM_, when)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Database.RocksDB (DB, Snapshot, createSnapshot, releaseSnapshot)+import Haskoin.Store.Database.Reader (DatabaseReader (..))+import UnliftIO+  ( TVar,+    atomically,+    bracket,+    mask_,+    newTVarIO,+    readTVar,+    readTVarIO,+    stateTVar,+    writeTVar,+  )++-- | The views of a database, by block.+data Views b = Views+  { reader :: !DatabaseReader,+    held :: !(TVar (Map b Held))+  }++-- | A snapshot, the number of readers that are using it, and whether it is+-- to be released when the last of them is done.+data Held = Held+  { snapshot :: !(DB, Snapshot),+    readers :: !Int,+    doomed :: !Bool+  }++-- | No views yet, of a database.+newViews :: DatabaseReader -> IO (Views b)+newViews reader = Views reader <$> newTVarIO Map.empty++-- | Take a view of the database as it is now, for a block. The caller must+-- be the one that writes to the database, between the import of that block+-- and its next write, so that the view is of that block and nothing else.+-- A view that the block already has is kept, and is no longer to be+-- released: a block that comes back after a reorganisation is the same+-- block.+takeView :: (Ord b) => Views b -> b -> IO ()+takeView views block = mask_ $ do+  fresh <- createSnapshot views.reader.db+  spare <- atomically . stateTVar views.held $ \m -> case Map.lookup block m of+    Just old -> (True, Map.insert block old {doomed = False} m)+    Nothing -> (False, Map.insert block Held {snapshot = fresh, readers = 0, doomed = False} m)+  when spare (releaseSnapshot fresh)++-- | The blocks that there is a view of and that is not on its way out.+heldViews :: Views b -> IO [b]+heldViews views = Map.keys . Map.filter (not . (.doomed)) <$> readTVarIO views.held++-- | Let go of every view but those of the given blocks. A view that is+-- being read is released when its last reader is done, and counts as gone+-- from now on.+keepViews :: (Ord b) => Views b -> [b] -> IO ()+keepViews views blocks = mask_ $ do+  free <- atomically . stateTVar views.held $ \m ->+    let (stay, go) = Map.partitionWithKey (\b _ -> b `elem` blocks) m+        (idle, busy) = Map.partition ((== 0) . (.readers)) go+     in (Map.elems idle, stay <> Map.map (\h -> h {doomed = True}) busy)+  forM_ free (releaseSnapshot . (.snapshot))++-- | Run an action with the database as it was at a block, if there is a+-- view of it: a reader whose every read goes through the snapshot. The+-- reader must not be used after the action returns.+withViewAt :: (Ord b) => Views b -> b -> (DatabaseReader -> IO a) -> IO (Maybe a)+withViewAt views block action = bracket enter leave (mapM run)+  where+    enter = atomically $ do+      m <- readTVar views.held+      case Map.lookup block m of+        Just h | not h.doomed -> do+          writeTVar views.held (Map.insert block h {readers = h.readers + 1} m)+          return (Just h)+        _ -> return Nothing+    run h = action views.reader {db = fst h.snapshot}+    leave Nothing = return ()+    leave (Just _) = do+      free <- atomically . stateTVar views.held $ \m -> case Map.lookup block m of+        Just h+          | h.readers <= 1 && h.doomed -> (Just h, Map.delete block m)+          | otherwise -> (Nothing, Map.insert block h {readers = h.readers - 1} m)+        Nothing -> (Nothing, m)+      forM_ free (releaseSnapshot . (.snapshot))++-- | Release every view. None may be in use.+closeViews :: Views b -> IO ()+closeViews views = mask_ $ do+  m <- atomically (stateTVar views.held (,Map.empty))+  forM_ m (releaseSnapshot . (.snapshot))
src/Haskoin/Store/Database/Writer.hs view
@@ -1,12 +1,4 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-}  -- | -- Module      : Haskoin.Store.Database.Writer@@ -18,11 +10,15 @@ -- -- Buffered writes to the database. ----- 'WriterT' implements 'StoreWrite' by recording every change in in-memory hash--- tables, and 'StoreReadBase' by reading those tables first and falling back to--- the database. When the action given to 'runWriter' finishes, all the changes--- are written to RocksDB in a single atomic batch. A block or a set of--- transactions is thus either fully imported or not at all.+-- A 'Writer' records every change in in-memory hash tables, and its reads look+-- in those tables first and fall back to the database. When the function given+-- to 'runWriter' returns, all the changes are written to RocksDB in a single+-- atomic batch. A block or a set of transactions is thus either fully imported+-- or not at all.+--+-- The writer is a plain handle passed to plain functions in 'IO' ('getTxData',+-- 'insertTx', ...), rather than a monad transformer with class instances: the+-- import logic says which writer it uses, and needs nothing but 'IO' to run. -- = Why buffer writes -- -- Importing a block touches thousands of keys, and many of them several times:@@ -30,23 +26,84 @@ -- transaction of its address. Buffering collapses those into one final value per -- key, and a single atomic batch makes the whole import all-or-nothing. A crash -- or an invalid block leaves the database exactly as it was.+--+-- = Two layouts+--+-- The writes of the first groups below are to the column families that the+-- store uses today, where the chain and the mempool share their indices.+-- The last group is for the layered layout of @docs\/layered-database.md@,+-- in which the mempool has an area of its own: bodies, spenders, address+-- entries and deleted transactions of the mempool. A batch can write to+-- both areas, and is still one atomic write. Only+-- "Haskoin.Store.Logic.Layered" uses that group, on a database opened with+-- 'Haskoin.Store.Database.Reader.withLayeredReader'; a batch that does not+-- use it writes nothing there. module Haskoin.Store.Database.Writer-  ( WriterT,+  ( -- * Batches+    Writer,     MempoolChanges (..),     runWriter,     runWriterMempool,+    writerNet,+    writerCtx,     writerLogger,++    -- * Reads, with the changes of the batch applied+    getBestBlock,+    getBlocksAtHeight,+    getBlock,+    getTxData,+    getSpender,+    getUnspent,+    getBalance,+    getMempool,++    -- * Writes+    setBest,+    insertBlock,+    setBlocksAtHeight,+    insertTx,+    insertAddrTx,+    deleteAddrTx,+    insertAddrUnspent,+    deleteAddrUnspent,+    addToMempool,+    deleteFromMempool,+    inMempool,+    setBalance,+    insertUnspent,+    deleteUnspent,+    deleteTxData,+    deleteBalance,++    -- * The mempool area of the layered layout+    getMempoolTx,+    getMempoolSpender,+    getMempoolSpenders,+    getChainSpender,+    getChainSpenders,+    insertSpender,+    deleteSpender,+    insertMempoolTx,+    deleteMempoolTx,+    insertMempoolSpender,+    deleteMempoolSpender,+    insertMempoolAddr,+    deleteMempoolAddr,+    getMempoolAddr,   ) where  import Control.Monad (forM)-import Control.Monad.Reader (ReaderT (..))-import Control.Monad.Reader qualified as R+import Data.Functor ((<&>)) import Data.HashMap.Strict qualified as HashMap import Data.HashTable.IO qualified as H+import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IntMap import Data.List (sortOn)+import Data.Maybe (catMaybes, isJust) import Data.Ord (Down (..))+import Data.Word (Word32) import Database.RocksDB (BatchOp, DB) import Database.RocksDB.Query   ( deleteOp,@@ -68,20 +125,21 @@     txHash,   ) import Haskoin.Node (Logger)-import Haskoin.Store.Common import Haskoin.Store.Data+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Query qualified as Q import Haskoin.Store.Database.Reader import Haskoin.Store.Database.Types import UnliftIO   ( IORef,-    MonadIO,     liftIO,     newIORef,     readIORef,     writeIORef,   ) --- | Environment of a batch of writes.+-- | A batch of writes: the database it reads from and writes to, the+-- changes made so far, and where to log. Make one with 'runWriter'. data Writer = Writer   { -- | database to read from and write to     reader :: !DatabaseReader,@@ -91,24 +149,17 @@     logger :: !Logger   } --- | Monad transformer for a batch of writes. Run it with 'runWriter'.-type WriterT = ReaderT Writer+-- | Network of the database of a batch.+writerNet :: Writer -> Network+writerNet w = w.reader.net --- | Logger passed to 'runWriter'.-writerLogger :: (Monad m) => WriterT m Logger-writerLogger = R.asks (.logger)+-- | Secp256k1 context of the database of a batch.+writerCtx :: Writer -> Ctx+writerCtx w = w.reader.ctx -instance (MonadIO m) => StoreReadBase (WriterT m) where-  getNetwork = R.asks (.reader.net)-  getCtx = R.asks (.reader.ctx)-  getBestBlock = getBestBlockI-  getBlocksAtHeight = getBlocksAtHeightI-  getBlock = getBlockI-  getTxData = getTxDataI-  getSpender = getSpenderI-  getUnspent = getUnspentI-  getBalance = getBalanceI-  getMempool = getMempoolI+-- | Logger passed to 'runWriter'.+writerLogger :: Writer -> Logger+writerLogger w = w.logger  -- | Change to the best block: 'Nothing' if unchanged, @Just Nothing@ if deleted. type BestRef = IORef (Maybe (Maybe BlockHash))@@ -137,6 +188,15 @@ -- | Mempool changes in this batch: 'Nothing' for a deletion. type MempoolTable = H.BasicHashTable TxHash (Maybe UnixTime) +-- | Changed groups of spenders of outputs in the chain.+type SpenderTable = H.BasicHashTable (TxHash, Word32) (IntMap Spender)++-- | Changed mempool spenders of outputs: 'Nothing' for a deletion.+type MemSpenderTable = H.BasicHashTable OutPoint (Maybe Spender)++-- | Changed entries of the mempool by address: 'Nothing' for a deletion.+type MemAddrTable = H.BasicHashTable MemAddrKey (Maybe MemAddrVal)+ -- | Changes made in a batch, not yet written to the database. data Memory = Memory   { -- | network constants@@ -160,77 +220,128 @@     -- | address unspent outputs     outputTable :: !AddrOutTable,     -- | mempool-    mempoolTable :: !MempoolTable+    mempoolTable :: !MempoolTable,+    -- | bodies of mempool transactions, in the mempool area+    memTxTable :: !TxTable,+    -- | mempool spenders of outputs, in the mempool area+    memSpenderTable :: !MemSpenderTable,+    -- | entries of the mempool by address, in the mempool area+    memAddrTable :: !MemAddrTable,+    -- | inputs of blocks that spend outputs, by group of outputs+    -- as t'SpenderKey' says, each group as the batch leaves it: empty for one+    -- that has no spent output left+    spenderTable :: !SpenderTable   } -instance (MonadIO m) => StoreWrite (WriterT m) where-  setBest h =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ writeIORef best (Just (Just h))-  insertBlock b =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert blockTable (headerHash b.header) (Just b)-  setBlocksAtHeight h g =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert heightTable g h-  insertTx t =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert txTable (txHash t.tx) (Just t)-  insertAddrTx a t =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert addressTable (a, t) (Just ())-  deleteAddrTx a t =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert addressTable (a, t) Nothing-  insertAddrUnspent a u =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert outputTable k (Just v)-    where-      k = (a, u.block, u.outpoint)-      v = OutVal u.value u.script-  deleteAddrUnspent a u =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert outputTable k Nothing-    where-      k = (a, u.block, u.outpoint)-  addToMempool x t =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert mempoolTable x (Just t)-  deleteFromMempool x =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert mempoolTable x Nothing-  setBalance b =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert balanceTable b.address (Just b)-  insertUnspent u =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert unspentTable (fst (unspentToVal u)) (Just u)-  deleteUnspent p =-    ReaderT $ \Writer {memory = Memory {..}} ->-      liftIO $ H.insert unspentTable p Nothing+-- | Set the best block.+setBest :: Writer -> BlockHash -> IO ()+setBest w h = writeIORef w.memory.best (Just (Just h)) --- | Read a value from the changes of the batch, or from the database if the batch did not change it.-getLayered ::-  (MonadIO m) =>-  (Memory -> IO (Maybe a)) ->-  DatabaseReaderT m a ->-  WriterT m a-getLayered f g =-  ReaderT $ \Writer {reader = db, memory = s} ->-    liftIO (f s) >>= \case-      Just x -> return x-      Nothing -> runReaderT g db+-- | Add or replace the data of a block.+insertBlock :: Writer -> BlockData -> IO ()+insertBlock w b = H.insert w.memory.blockTable (headerHash b.header) (Just b) +-- | Set the hashes of the blocks at a height.+setBlocksAtHeight :: Writer -> [BlockHash] -> BlockHeight -> IO ()+setBlocksAtHeight w h g = H.insert w.memory.heightTable g h++-- | Add or replace the data of a transaction.+insertTx :: Writer -> TxData -> IO ()+insertTx w t = H.insert w.memory.txTable (txHash t.tx) (Just t)++-- | Add a transaction to the index of an address.+insertAddrTx :: Writer -> Address -> TxRef -> IO ()+insertAddrTx w a t = H.insert w.memory.addressTable (a, t) (Just ())++-- | Remove a transaction from the index of an address.+deleteAddrTx :: Writer -> Address -> TxRef -> IO ()+deleteAddrTx w a t = H.insert w.memory.addressTable (a, t) Nothing++-- | Add an unspent output to the index of an address.+insertAddrUnspent :: Writer -> Address -> Unspent -> IO ()+insertAddrUnspent w a u =+  H.insert w.memory.outputTable (a, u.block, u.outpoint) (Just (OutVal u.value u.script))++-- | Remove an unspent output from the index of an address.+deleteAddrUnspent :: Writer -> Address -> Unspent -> IO ()+deleteAddrUnspent w a u =+  H.insert w.memory.outputTable (a, u.block, u.outpoint) Nothing++-- | Add a transaction to the mempool with the time it arrived.+addToMempool :: Writer -> TxHash -> UnixTime -> IO ()+addToMempool w x t = H.insert w.memory.mempoolTable x (Just t)++-- | Remove a transaction from the mempool.+deleteFromMempool :: Writer -> TxHash -> IO ()+deleteFromMempool w x = H.insert w.memory.mempoolTable x Nothing++-- | Set the balance of an address.+setBalance :: Writer -> Balance -> IO ()+setBalance w b = H.insert w.memory.balanceTable b.address (Just b)++-- | Add an unspent output.+insertUnspent :: Writer -> Unspent -> IO ()+insertUnspent w u = H.insert w.memory.unspentTable (fst (unspentToVal u)) (Just u)++-- | Remove an unspent output.+deleteUnspent :: Writer -> OutPoint -> IO ()+deleteUnspent w p = H.insert w.memory.unspentTable p Nothing++-- | Remove the data of a transaction.+deleteTxData :: Writer -> TxHash -> IO ()+deleteTxData w h = H.insert w.memory.txTable h Nothing++-- | Remove the balance of an address, as that of one never used.+deleteBalance :: Writer -> Address -> IO ()+deleteBalance w a = H.insert w.memory.balanceTable a Nothing++-- | Add or replace the body of a mempool transaction in the mempool area.+insertMempoolTx :: Writer -> TxData -> IO ()+insertMempoolTx w t = H.insert w.memory.memTxTable (txHash t.tx) (Just t)++-- | Remove the body of a mempool transaction from the mempool area.+deleteMempoolTx :: Writer -> TxHash -> IO ()+deleteMempoolTx w h = H.insert w.memory.memTxTable h Nothing++-- | Record the mempool input that spends an output.+insertMempoolSpender :: Writer -> OutPoint -> Spender -> IO ()+insertMempoolSpender w p s = H.insert w.memory.memSpenderTable p (Just s)++-- | Forget the mempool input that spends an output.+deleteMempoolSpender :: Writer -> OutPoint -> IO ()+deleteMempoolSpender w p = H.insert w.memory.memSpenderTable p Nothing++-- | Add an entry of the mempool by address.+insertMempoolAddr :: Writer -> MemAddrKey -> MemAddrVal -> IO ()+insertMempoolAddr w k v = H.insert w.memory.memAddrTable k (Just v)++-- | Remove an entry of the mempool by address.+deleteMempoolAddr :: Writer -> MemAddrKey -> IO ()+deleteMempoolAddr w k = H.insert w.memory.memAddrTable k Nothing++-- | An entry of the mempool by address, with the changes of the batch+-- applied.+getMempoolAddr :: Writer -> MemAddrKey -> IO (Maybe MemAddrVal)+getMempoolAddr w k = getLayered w (\s -> H.lookup s.memAddrTable k) (\r -> retrieveCF r.db (mempoolAddrCF r.db) k)++-- | Read a value from the changes of the batch, or from the database if the+-- batch did not change it.+getLayered :: Writer -> (Memory -> IO (Maybe a)) -> (DatabaseReader -> IO a) -> IO a+getLayered w f g =+  f w.memory >>= \case+    Just x -> return x+    Nothing -> g w.reader+ -- | Run a batch of writes and commit all its changes to the database in one--- atomic write. Reads within the batch see its own changes.+-- atomic write. Reads within the batch see its own changes. If the function+-- throws, nothing is written. runWriter ::-  (MonadIO m) =>   Network ->   Ctx ->   DatabaseReader ->   Logger ->-  WriterT m a ->-  m a+  (Writer -> IO a) ->+  IO a runWriter net ctx bdb logger f =   fst <$> runWriterMempool net ctx bdb logger f @@ -247,23 +358,22 @@ -- | Like 'runWriter', but also say how the batch changed the mempool, so -- that callers do not have to read the whole mempool before and after. runWriterMempool ::-  (MonadIO m) =>   Network ->   Ctx ->   DatabaseReader ->   Logger ->-  WriterT m a ->-  m (a, MempoolChanges)+  (Writer -> IO a) ->+  IO (a, MempoolChanges) runWriterMempool net ctx bdb@DatabaseReader {db} logger f = do   hm <- newMemory net ctx-  x <- R.runReaderT f Writer {reader = bdb, memory = hm, logger}+  x <- f Writer {reader = bdb, memory = hm, logger}   ops <- hashMapOps db hm   (mops, changes) <- mempoolOps db hm.mempoolTable-  liftIO $ writeBatch db (ops <> mops)+  writeBatch db (ops <> mops)   return (x, changes)  -- | Database operations for all changes of a batch except the mempool.-hashMapOps :: (MonadIO m) => DB -> Memory -> m [BatchOp]+hashMapOps :: DB -> Memory -> IO [BatchOp] hashMapOps db Memory {..} =   mconcat     <$> sequence@@ -274,11 +384,15 @@         balOps db balanceTable,         addrTxOps db addressTable,         addrOutOps db outputTable,-        unspentOps db unspentTable+        unspentOps db unspentTable,+        memTxOps db memTxTable,+        memSpenderOps db memSpenderTable,+        memAddrOps db memAddrTable,+        spenderOps db spenderTable       ]  -- | Operation for the change of the best block, if any.-bestBlockOp :: (MonadIO m) => BestRef -> m [BatchOp]+bestBlockOp :: BestRef -> IO [BatchOp] bestBlockOp r =   readIORef r >>= \case     Nothing -> return []@@ -286,42 +400,42 @@     Just (Just b) -> return [insertOp BestKey b]  -- | Operations for changed blocks.-blockHashOps :: (MonadIO m) => DB -> BlockTable -> m [BatchOp]-blockHashOps db t = map (uncurry f) <$> liftIO (H.toList t)+blockHashOps :: DB -> BlockTable -> IO [BatchOp]+blockHashOps db t = map (uncurry f) <$> (H.toList t)   where     f k (Just d) = insertOpCF (blockCF db) (BlockKey k) d     f k Nothing = deleteOpCF (blockCF db) (BlockKey k)  -- | Operations for changed block heights.-blockHeightOps :: (MonadIO m) => DB -> HeightTable -> m [BatchOp]-blockHeightOps db t = map (uncurry f) <$> liftIO (H.toList t)+blockHeightOps :: DB -> HeightTable -> IO [BatchOp]+blockHeightOps db t = map (uncurry f) <$> (H.toList t)   where     f = insertOpCF (heightCF db) . HeightKey  -- | Operations for changed transactions.-txOps :: (MonadIO m) => DB -> TxTable -> m [BatchOp]-txOps db t = map (uncurry f) <$> liftIO (H.toList t)+txOps :: DB -> TxTable -> IO [BatchOp]+txOps db t = map (uncurry f) <$> (H.toList t)   where     f k (Just t') = insertOpCF (txCF db) (TxKey k) t'     f k Nothing = deleteOpCF (txCF db) (TxKey k)  -- | Operations for changed balances.-balOps :: (MonadIO m) => DB -> BalanceTable -> m [BatchOp]-balOps db t = map (uncurry f) <$> liftIO (H.toList t)+balOps :: DB -> BalanceTable -> IO [BatchOp]+balOps db t = map (uncurry f) <$> (H.toList t)   where     f a (Just b) = insertOpCF (balanceCF db) (BalKey a) (balanceToVal b)     f a Nothing = deleteOpCF (balanceCF db) (BalKey a)  -- | Operations for changed address transactions.-addrTxOps :: (MonadIO m) => DB -> AddrTxTable -> m [BatchOp]-addrTxOps db t = map (uncurry f) <$> liftIO (H.toList t)+addrTxOps :: DB -> AddrTxTable -> IO [BatchOp]+addrTxOps db t = map (uncurry f) <$> (H.toList t)   where     f (a, t') (Just ()) = insertOpCF (addrTxCF db) (AddrTxKey a t') ()     f (a, t') Nothing = deleteOpCF (addrTxCF db) (AddrTxKey a t')  -- | Operations for changed address unspent outputs.-addrOutOps :: (MonadIO m) => DB -> AddrOutTable -> m [BatchOp]-addrOutOps db t = map (uncurry f) <$> liftIO (H.toList t)+addrOutOps :: DB -> AddrOutTable -> IO [BatchOp]+addrOutOps db t = map (uncurry f) <$> (H.toList t)   where     f (a, b, p) (Just l) =       insertOpCF@@ -337,8 +451,8 @@ -- compared to the database: its time is needed to update the index by -- time, and the comparison says which transactions really entered or left -- the mempool.-mempoolOps :: (MonadIO m) => DB -> MempoolTable -> m ([BatchOp], MempoolChanges)-mempoolOps db t = liftIO $ do+mempoolOps :: DB -> MempoolTable -> IO ([BatchOp], MempoolChanges)+mempoolOps db t = do   changes <- H.toList t   rows <- forM changes $ \(h, new) -> do     old <- retrieveCF db cf (MemTxKey h)@@ -360,62 +474,175 @@       | o == n = []       | otherwise = deleteOpCF cf (MemTimeKey o h) : mempoolInsertOps db h n +-- | Operations for changed bodies of mempool transactions.+memTxOps :: DB -> TxTable -> IO [BatchOp]+memTxOps db t = map (uncurry f) <$> H.toList t+  where+    f h (Just td) = insertOpCF (mempoolCF db) (TxKey h) td+    f h Nothing = deleteOpCF (mempoolCF db) (TxKey h)++-- | Operations for the changed groups of spenders of the chain.+spenderOps :: DB -> SpenderTable -> IO [BatchOp]+spenderOps db t = map (uncurry f) <$> H.toList t+  where+    f (h, g) m+      | IntMap.null m = deleteOpCF (spenderCF db) (SpenderKey h g)+      | otherwise = insertOpCF (spenderCF db) (SpenderKey h g) (SpenderVal m)++-- | Operations for changed mempool spenders.+memSpenderOps :: DB -> MemSpenderTable -> IO [BatchOp]+memSpenderOps db t = map (uncurry f) <$> H.toList t+  where+    f p (Just s) = insertOpCF (mempoolCF db) (MemSpenderKey p) s+    f p Nothing = deleteOpCF (mempoolCF db) (MemSpenderKey p)++-- | Operations for changed entries of the mempool by address. Their column+-- family is only looked up if there are any, as a database of the layout in+-- use today does not have it.+memAddrOps :: DB -> MemAddrTable -> IO [BatchOp]+memAddrOps db t = map (uncurry f) <$> H.toList t+  where+    f k (Just v) = insertOpCF (mempoolAddrCF db) k v+    f k Nothing = deleteOpCF (mempoolAddrCF db) k+ -- | Operations for changed unspent outputs.-unspentOps :: (MonadIO m) => DB -> UnspentTable -> m [BatchOp]-unspentOps db t = map (uncurry f) <$> liftIO (H.toList t)+unspentOps :: DB -> UnspentTable -> IO [BatchOp]+unspentOps db t = map (uncurry f) <$> (H.toList t)   where     f p (Just u) =       insertOpCF (unspentCF db) (UnspentKey p) (snd (unspentToVal u))     f p Nothing =       deleteOpCF (unspentCF db) (UnspentKey p) --- | Read the best block, with the changes of the batch applied.-getBestBlockI :: (MonadIO m) => WriterT m (Maybe BlockHash)-getBestBlockI = getLayered getBestBlockH getBestBlock+-- | Best block, with the changes of the batch applied.+getBestBlock :: Writer -> IO (Maybe BlockHash)+getBestBlock w = getLayered w getBestBlockH Q.getBestBlock --- | Read the blocks at a height, with the changes of the batch applied.-getBlocksAtHeightI :: (MonadIO m) => BlockHeight -> WriterT m [BlockHash]-getBlocksAtHeightI bh =-  getLayered (getBlocksAtHeightH bh) (getBlocksAtHeight bh)+-- | Blocks at a height, with the changes of the batch applied.+getBlocksAtHeight :: Writer -> BlockHeight -> IO [BlockHash]+getBlocksAtHeight w bh = getLayered w (getBlocksAtHeightH bh) (`Q.getBlocksAtHeight` bh) --- | Read a block, with the changes of the batch applied.-getBlockI :: (MonadIO m) => BlockHash -> WriterT m (Maybe BlockData)-getBlockI bh = getLayered (getBlockH bh) (getBlock bh)+-- | Data of a block, with the changes of the batch applied.+getBlock :: Writer -> BlockHash -> IO (Maybe BlockData)+getBlock w bh = getLayered w (getBlockH bh) (`Q.getBlock` bh) --- | Read a transaction, with the changes of the batch applied.-getTxDataI :: (MonadIO m) => TxHash -> WriterT m (Maybe TxData)-getTxDataI th = getLayered (getTxDataH th) (getTxData th)+-- | Data of a transaction, deleted ones included, with the changes of the+-- batch applied.+getTxData :: Writer -> TxHash -> IO (Maybe TxData)+getTxData w th = getLayered w (getTxDataH th) (`Q.getTxRecord` th) --- | Read the spender of an output, with the changes of the batch applied.-getSpenderI :: (MonadIO m) => OutPoint -> WriterT m (Maybe Spender)-getSpenderI op = getLayered (getSpenderH op) (getSpender op)+-- | Spender of an output, with the changes of the batch applied.+getSpender :: Writer -> OutPoint -> IO (Maybe Spender)+getSpender w op = getLayered w (getSpenderH op) fromRecord+  where+    -- the rules of version 20 keep spenders in the record of the+    -- transaction that made the output, and this is theirs+    fromRecord r = (>>= IntMap.lookup (fromIntegral op.index) . (.spenders)) <$> Q.getTxRecord r op.hash --- | Read the balance of an address, with the changes of the batch applied.-getBalanceI :: (MonadIO m) => Address -> WriterT m (Maybe Balance)-getBalanceI a = getLayered (getBalanceH a) (getBalance a)+-- | Balance of an address, with the changes of the batch applied.+getBalance :: Writer -> Address -> IO (Maybe Balance)+getBalance w a = getLayered w (getBalanceH a) (`Q.getBalance` a) --- | Read an unspent output, with the changes of the batch applied.-getUnspentI :: (MonadIO m) => OutPoint -> WriterT m (Maybe Unspent)-getUnspentI op = getLayered (getUnspentH op) (getUnspent op)+-- | Unspent output, with the changes of the batch applied.+getUnspent :: Writer -> OutPoint -> IO (Maybe Unspent)+getUnspent w op = getLayered w (getUnspentH op) (`Q.getUnspent` op) +-- | Whether a transaction is in the mempool, with the changes of the batch+-- applied. It reads the small key that lists the transaction, where+-- 'getMempoolTx' reads and decodes its whole body: a rule that only needs+-- to know asks this.+inMempool :: Writer -> TxHash -> IO Bool+inMempool w h =+  H.lookup w.memory.mempoolTable h >>= \case+    Just change -> return (isJust change)+    Nothing -> isJust <$> (retrieveCF w.reader.db (mempoolCF w.reader.db) (MemTxKey h) :: IO (Maybe UnixTime))++-- | Body of a transaction in the mempool area, without its spenders, with+-- the changes of the batch applied.+getMempoolTx :: Writer -> TxHash -> IO (Maybe TxData)+getMempoolTx w h = getLayered w (\s -> H.lookup s.memTxTable h) (`M.getMempoolTx` h)++-- | Mempool input that spends an output, with the changes of the batch+-- applied.+getMempoolSpender :: Writer -> OutPoint -> IO (Maybe Spender)+getMempoolSpender w p = getLayered w (\s -> H.lookup s.memSpenderTable p) stored+  where+    stored r = retrieveCF r.db (mempoolCF r.db) (MemSpenderKey p)++-- | Mempool inputs that spend the outputs of a transaction, by output+-- index, with the changes of the batch applied. The second argument is the+-- number of outputs of the transaction: what the database has for it is+-- read with one scan, and the changes of the batch are looked up for each+-- output.+getMempoolSpenders :: Writer -> TxHash -> Int -> IO (IntMap Spender)+getMempoolSpenders w h outputs = do+  stored <- M.getMempoolSpenders w.reader h+  changed <- catMaybes <$> mapM change [0 .. outputs - 1]+  return $ IntMap.mapMaybe id (IntMap.union (IntMap.fromList changed) (IntMap.map Just stored))+  where+    change i = fmap (i,) <$> H.lookup w.memory.memSpenderTable (OutPoint h (fromIntegral i))++-- | The spenders of a group of outputs of a transaction, with the changes+-- of the batch applied.+spenderGroupOf :: Writer -> TxHash -> Word32 -> IO (IntMap Spender)+spenderGroupOf w h g =+  H.lookup w.memory.spenderTable (h, g) >>= \case+    Just m -> return m+    Nothing ->+      retrieveCF w.reader.db (spenderCF w.reader.db) (SpenderKey h g) <&> \case+        Just (SpenderVal m) -> m+        Nothing -> IntMap.empty++-- | Note that an input of a block spends an output, in the group of+-- outputs that it belongs to. The record of the transaction that made the+-- output is not read and not written.+insertSpender :: Writer -> OutPoint -> Spender -> IO ()+insertSpender w p s = do+  let g = p.index `div` spenderGroup+  m <- spenderGroupOf w p.hash g+  H.insert w.memory.spenderTable (p.hash, g) (IntMap.insert (fromIntegral p.index) s m)++-- | Note that no block spends an output any more.+deleteSpender :: Writer -> OutPoint -> IO ()+deleteSpender w p = do+  let g = p.index `div` spenderGroup+  m <- spenderGroupOf w p.hash g+  H.insert w.memory.spenderTable (p.hash, g) (IntMap.delete (fromIntegral p.index) m)++-- | The input of a block that spends an output, with the changes of the+-- batch applied.+getChainSpender :: Writer -> OutPoint -> IO (Maybe Spender)+getChainSpender w p = IntMap.lookup (fromIntegral p.index) <$> spenderGroupOf w p.hash (p.index `div` spenderGroup)++-- | The inputs of blocks that spend the outputs of a transaction, by+-- output, with the changes of the batch applied, given the number of its+-- outputs: what the database has, read with one scan, with the groups+-- that the batch changed in place of the stored ones.+getChainSpenders :: Writer -> TxHash -> Int -> IO (IntMap Spender)+getChainSpenders w h outputs = do+  stored <- Q.getChainSpenders w.reader h+  let groups = [0 .. (fromIntegral (max 1 outputs) - 1) `div` spenderGroup]+  changed <- catMaybes <$> mapM (\g -> fmap (g,) <$> H.lookup w.memory.spenderTable (h, g)) groups+  let replaced = [g | (g, _) <- changed]+      kept = IntMap.filterWithKey (\i _ -> (fromIntegral i `div` spenderGroup) `notElem` replaced) stored+  return (IntMap.unions (kept : map snd changed))+ -- | The mempool in the database with the changes of this batch applied, -- newest first.-getMempoolI :: (MonadIO m) => WriterT m [(UnixTime, TxHash)]-getMempoolI =-  ReaderT $ \Writer {reader = db, memory = Memory {..}} -> do-    stored <- runReaderT getMempool db-    changes <- liftIO $ H.toList mempoolTable-    let changed = HashMap.fromList changes-        kept = filter (not . (`HashMap.member` changed) . snd) stored-        added = [(t, h) | (h, Just t) <- changes]-    return $ sortOn Down (kept <> added)+getMempool :: Writer -> IO [(UnixTime, TxHash)]+getMempool w = do+  stored <- Q.getMempool w.reader+  changes <- H.toList w.memory.mempoolTable+  let changed = HashMap.fromList changes+      kept = filter (not . (`HashMap.member` changed) . snd) stored+      added = [(t, h) | (h, Just t) <- changes]+  return $ sortOn Down (kept <> added)  -- | Empty set of changes. newMemory ::-  (MonadIO m) =>   Network ->   Ctx ->-  m Memory+  IO Memory newMemory net ctx = do   best <- newIORef Nothing   blockTable <- liftIO H.new@@ -426,6 +653,10 @@   addressTable <- liftIO H.new   outputTable <- liftIO H.new   mempoolTable <- liftIO H.new+  memTxTable <- liftIO H.new+  memSpenderTable <- liftIO H.new+  memAddrTable <- liftIO H.new+  spenderTable <- liftIO H.new   return Memory {..}  -- | Read the best block from the changes of the batch: 'Nothing' if unchanged, @Just Nothing@ if deleted.
+ src/Haskoin/Store/Fleet.hs view
@@ -0,0 +1,201 @@+-- |+-- Module      : Haskoin.Store.Fleet+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- How stores that share one Redis agree on the block that they all answer+-- from, as pure decisions. "Haskoin.Store.Fleet.Redis" carries them out.+-- This is the first part of the design in @docs\/layered-database.md@, "A+-- fleet that answers as one", written down so that it can be tested before+-- a store depends on it.+--+-- = The problem+--+-- Stores import the same blocks at slightly different times. Between the+-- moment the first of them has a block and the moment the last has it, the+-- same request gets different answers depending on which store a load+-- balancer picks. Making the stores commit a block together would need+-- every one of them to be there and on time, and one that stalls would hold+-- up all the others.+--+-- = The idea: agree on the state, not on the moment+--+-- Redis holds the /fleet tip/: the newest block that every live store has.+-- Each store imports blocks whenever it gets them, and keeps a view of its+-- database at each recent block. A request reads the fleet tip and is+-- answered from the view at that block. So every store gives the same+-- answer, and the fleet moves to a new block in one step, when the tip+-- moves, although no two stores imported it at the same time.+--+-- = The rules+--+-- * __A store reports its recent blocks__, newest first, under a lease that+--   it has to renew ('report'). A store that dies or hangs stops counting+--   when its lease runs out, and holds nobody up. It reports only the blocks+--   that it still has a view of: a view that was let go cannot be had again,+--   and the model test found at once what happens when the tip moves to such+--   a block. A store that is alive but stuck on old blocks would hold the+--   tip back for as long as it renews its lease, so a store stops reporting+--   when its own health checks fail.+--+-- * __A store reports only if it has a view of the fleet tip__ ('mayReport').+--   One that is catching up, or that just started, would otherwise pull the+--   tip back to an older block, or to one that it alone has.+--+-- * __The tip moves to the newest block that every live store reported__+--   ('agreed', 'move'). Any store may move it. The move is a compare and+--   set, so two stores that try at once do the same thing once.+--+-- * __A store keeps the views of the blocks of its last report, and of the+--   tip that it read after making it__ ('keep'). The tip can only move to a+--   block that every live store reported, so a live store always has the+--   view that it is asked for.+--+-- * __A store that does not have a view of the tip does not answer from+--   another block__ ('answer'). It was away for longer than its lease, and+--   says that it is behind until it has caught up.+--+-- A reorganisation needs no rule of its own. The tip is a block, not a+-- height: when every live store has moved to the other branch, the newest+-- block that they all report is on it, and the tip goes there.+--+-- The stores and the blocks can be of any type that can be compared, so+-- that the tests can run fleets of small numbers.+module Haskoin.Store.Fleet+  ( -- * The state in Redis+    Fleet (..),+    Report (..),+    Time,+    empty,++    -- * Reporting+    mayReport,+    report,+    live,++    -- * Moving the tip+    agreed,+    Move (..),+    move,++    -- * In a store+    keep,+    Answer (..),+    answer,+  )+where++import Data.List (find)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Word (Word64)++-- | A moment, in any unit that the stores share. In Redis it is the clock+-- of Redis, so that the stores do not have to agree on theirs.+type Time = Word64++-- | What a store last said about itself.+data Report b = Report+  { -- | the blocks of its main chain that it has views of, newest first+    recent :: ![b],+    -- | the moment its lease runs out+    expires :: !Time+  }+  deriving (Eq, Show)++-- | What Redis holds for a fleet of stores of type @s@ with blocks of type+-- @b@.+data Fleet s b = Fleet+  { -- | the block that every store answers from, once there is one+    tip :: !(Maybe b),+    -- | the last report of each store, live or not+    reports :: !(Map s (Report b))+  }+  deriving (Eq, Show)++-- | A fleet that no store has joined.+empty :: Fleet s b+empty = Fleet {tip = Nothing, reports = Map.empty}++-- | The reports whose lease has not run out.+live :: Time -> Fleet s b -> Map s (Report b)+live now fleet = Map.filter ((> now) . (.expires)) fleet.reports++-- | Whether a store may report, given a test for the blocks that it has a+-- view of. It may when it has a view of the tip. It also may when there is+-- no tip yet, or when no other store is live: then there is nobody to+-- disagree with, and a fleet whose stores were all away has to be able to+-- start again.+mayReport :: (Ord s) => Time -> s -> (b -> Bool) -> Fleet s b -> Bool+mayReport now store has fleet =+  maybe True has fleet.tip || Map.null (Map.delete store (live now fleet))++-- | Note the recent blocks of a store, newest first, under a lease of some+-- length. The store must have checked 'mayReport' against the same state:+-- in Redis the two are one script.+report :: (Ord s) => Time -> Time -> s -> [b] -> Fleet s b -> Fleet s b+report now lease store blocks fleet =+  fleet {reports = Map.insert store Report {recent = blocks, expires = now + lease} fleet.reports}++-- | The newest block that every live store reported, if there is one, and+-- if any store is live. Stores report their blocks newest first, so it is+-- the first block of any one report that all the others have.+agreed :: (Eq b) => Time -> Fleet s b -> Maybe b+agreed now fleet = case Map.elems (live now fleet) of+  [] -> Nothing+  first : others -> find (\b -> all (elem b . (.recent)) others) first.recent++-- | What became of an attempt to move the tip.+data Move s b+  = -- | the tip is now this block+    Moved !(Fleet s b)+  | -- | the tip is already where the live stores agree, or they agree on+    -- nothing+    Stays+  | -- | the tip was not the one that the store expected: another store moved+    -- it first+    Raced+  deriving (Eq, Show)++-- | Move the tip to the block that the live stores agree on, given the tip+-- that the caller saw. The caller says what it saw because it also changes+-- the shared mempool for the blocks between the two tips, and that change+-- is right for one starting point only.+move :: (Eq b) => Time -> Maybe b -> Fleet s b -> Move s b+move now seen fleet+  | seen /= fleet.tip = Raced+  | otherwise = case agreed now fleet of+      Just b | Just b /= fleet.tip -> Moved fleet {tip = Just b}+      _ -> Stays++-- | The blocks whose views a store must keep: those of its last report,+-- and the tip that it read after that report was made. It may let go of+-- every other view.+--+-- The order matters. A tip read before the report could have moved, in+-- between, to a block that only the earlier report had.+keep :: [b] -> Maybe b -> [b]+keep reported seen = maybe reported (: reported) seen++-- | What a store answers a request from.+data Answer b+  = -- | the view at this block, as every other store does+    At !b+  | -- | its own best block: there is no fleet tip yet+    Alone+  | -- | nothing: it has no view of the fleet tip, so any answer would differ+    -- from those of the other stores+    Behind+  deriving (Eq, Show)++-- | What a store answers a request from, given a test for the blocks that+-- it has a view of and the fleet as the request found it.+answer :: (b -> Bool) -> Fleet s b -> Answer b+answer has fleet = case fleet.tip of+  Nothing -> Alone+  Just b+    | has b -> At b+    | otherwise -> Behind
+ src/Haskoin/Store/Fleet/Member.hs view
@@ -0,0 +1,1078 @@+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Haskoin.Store.Fleet.Member+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- A store as a member of a fleet: what it does, with its own database and+-- the Redis that the fleet shares, so that every store of the fleet gives+-- the same answer to the same request. The rules are in+-- "Haskoin.Store.Fleet" and "Haskoin.Store.Fleet.Pool", Redis carries them+-- out through "Haskoin.Store.Fleet.Redis", and the reasons are in "A fleet+-- that answers as one" in @docs\/layered-database.md@.+--+-- = What a member does+--+-- * __It keeps a view of its database at each recent block__ ('noteBest'),+--   taken by the block store right after it imports or reverts a block,+--   and lets go of those that the rules no longer ask for.+--+-- * __It reports those blocks under a lease, and moves the fleet tip__+--   ('memberTick') to the newest block that every live store reported,+--   telling the pool what the blocks in between did ('changeBetween'):+--   which transactions they confirmed and which outputs they spent, and+--   across a reorganisation which transactions come back.+--+-- * __It offers its mempool to the shared pool__ ('offerMempool'), each+--   transaction with whether its inputs are unspent in the store's view at+--   the fleet tip, so that the pool only takes what is valid there.+--+-- * __It answers from the view at the fleet tip with the pool on top__+--   ('answering', 'redisPool'), or says that it is behind when it has no+--   view of the tip.+--+-- * __It keeps the fleet's cache of extended public keys__+--   ("Haskoin.Store.Fleet.XPub"): it answers the queries about a key from+--   it when the cache has the key ('answeringCached'), builds the entries+--   that requests missed ('memberCacheLoop'), brings the cache to the new+--   block when it moves the tip, and empties a cache that was left behind+--   ('syncCache').+--+-- = Why the member does not withdraw what it deletes+--+-- A store deletes a transaction from its own mempool when it replaces it,+-- when a block confirms a conflicting one, and when it is too old. The+-- first reaches the pool with the offer of the replacement, which names+-- what it replaces. The second reaches it when the tip moves to that block.+-- The third is done on the pool itself, by its own times ('expirePool'), so+-- that the stores do not have to agree on when each of them first saw a+-- transaction. A store that simply does not have a transaction says+-- nothing about it: another store has it.+module Haskoin.Store.Fleet.Member+  ( -- * A member+    Member (..),+    MemberConfig (..),+    newMember,+    noteBest,+    memberForget,+    memberTick,+    memberLoop,+    memberAlive,+    memberEvents,++    -- * Answering+    Standing (..),+    answering,+    memberTip,+    answeringAt,+    answeringCached,+    TipMoved (..),+    redisPool,++    -- * The cache of extended public keys+    CacheMetrics (..),+    memberCacheLoop,+    wantCached,+    syncCache,++    -- * The parts of a tick+    report,+    moveTip,+    Between (..),+    blocksBetween,+    changeBetween,+    offerMempool,+    expirePool,+    poolTx,+  )+where++import Control.Exception (Exception, throwIO)+import Control.Monad (filterM, forM, forM_, unless, void, when)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Char8 qualified as C+import Data.Either (rights)+import Data.Functor ((<&>))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict qualified as IntMap+import Data.List (nub, sortOn)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe)+import Data.Serialize (Serialize, decode, encode)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding (decodeUtf8)+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word64)+import Database.Redis qualified as R+import Haskoin+  ( BlockHash,+    BlockHeader (..),+    Ctx,+    Network (..),+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    blockHashToHex,+    headerHash,+    nullOutPoint,+    txHash,+  )+import Haskoin.Node (LogLevel (..))+import Haskoin.Store.Common (Limits (..), StoreRead (..))+import Haskoin.Store.Data+  ( Balance (..),+    BlockData (..),+    BlockRef (..),+    Spender (..),+    TxData (..),+    UnixTime,+    Unspent (..),+    XPubBal (..),+    XPubSpec,+    nullBalance,+  )+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Query.Pooled (PoolReads (..), pageStart, pooledRead)+import Haskoin.Store.Database.Reader (DatabaseReader (..), txCF)+import Haskoin.Store.Database.Types+  ( MemAddrKey (..),+    MemAddrVal (..),+    MemSummary (..),+    TxKey (..),+  )+import Haskoin.Store.Database.Views (Views, heldViews, keepViews, newViews, takeView, withViewAt)+import Haskoin.Store.Fleet qualified as F+import Haskoin.Store.Fleet.Redis+import Haskoin.Store.Fleet.XPub+import System.Metrics.StatsD+  ( StatCounter,+    StatGauge,+    StatTiming,+    Stats,+    addTiming,+    incrementCounter,+    newStatCounter,+    newStatGauge,+    newStatTiming,+    setGauge,+  )+import UnliftIO+  ( TVar,+    atomically,+    handle,+    handleAny,+    modifyIORef',+    modifyTVar',+    newIORef,+    newTVarIO,+    readIORef,+    readTVar,+    readTVarIO,+    retrySTM,+    stateTVar,+    timeout,+    writeIORef,+    writeTVar,+  )++-- | How a store takes part in a fleet.+data MemberConfig = MemberConfig+  { -- | what every key of the fleet in Redis starts with+    prefix :: !ByteString,+    -- | the name of this store, which no other store of the fleet has+    name :: !ByteString,+    -- | how long a report counts for, in milliseconds. A store that stops+    -- reporting holds the tip back for this long at most.+    lease :: !Word64,+    -- | how many recent blocks to keep views of and report. With more, the+    -- stores find a block in common sooner when they import several at+    -- once; each view keeps the files that it reads from being deleted.+    depth :: !Int,+    -- | seconds after which a transaction leaves the pool unconfirmed, or 0+    -- for never+    expiry :: !Word64,+    -- | how many extended public keys the fleet's cache of them may hold+    -- ("Haskoin.Store.Fleet.XPub"), the least recently used going first, or+    -- 0 not to use the cache+    cacheMax :: !Int,+    -- | cache a key that has at least this many used addresses+    cacheMinAddrs :: !Int,+    -- | or at least this many transactions in its addresses+    cacheMinTxs :: !Int,+    -- | StatsD client, to report what the cache of extended public keys+    -- does, as t'CacheMetrics' lists+    stats :: !(Maybe Stats),+    -- | where to say what a log should have: that the cache was emptied+    -- and why, as information, and each entry built and each move of the+    -- cache, for debugging+    note :: !(LogLevel -> Text -> IO ())+  }++-- | StatsD metrics of the fleet's cache of extended public keys, as this+-- store sees it. They have the names of those of "Haskoin.Store.Cache"+-- under @fleet@, so that a dashboard reads both alike.+data CacheMetrics = CacheMetrics+  { -- | requests about a key whose balances the cache gave+    -- (@fleet.cache.hits@)+    hits :: !StatCounter,+    -- | requests about a key that the database answered: the cache did+    -- not have the key, had it with other addresses, or was not at the+    -- fleet tip (@fleet.cache.misses@)+    misses :: !StatCounter,+    -- | entries that this store built or added addresses to+    -- (@fleet.cache.builds@)+    builds :: !StatCounter,+    -- | builds that this store gave up, because the tip moved or another+    -- store wrote the entry (@fleet.cache.abandoned@)+    abandoned :: !StatCounter,+    -- | entries that this store deleted for being the least recently used+    -- (@fleet.cache.evictions@)+    evictions :: !StatCounter,+    -- | times that this store emptied the cache (@fleet.cache.emptied@)+    emptied :: !StatCounter,+    -- | milliseconds to build an entry (@fleet.cache.build@)+    buildTime :: !StatTiming,+    -- | entries in the cache, one for each extended public key, whichever+    -- store built them (@fleet.cache.xpubs@)+    xpubs :: !StatGauge,+    -- | addresses that those entries have between them+    -- (@fleet.cache.addresses@)+    addresses :: !StatGauge+  }++-- | Register the metrics of the cache.+newCacheMetrics :: Stats -> IO CacheMetrics+newCacheMetrics s = do+  hits <- c "hits"+  misses <- c "misses"+  builds <- c "builds"+  abandoned <- c "abandoned"+  evictions <- c "evictions"+  emptied <- c "emptied"+  buildTime <- newStatTiming s "fleet.cache.build" 10+  xpubs <- newStatGauge s "fleet.cache.xpubs" 0+  addresses <- newStatGauge s "fleet.cache.addresses" 0+  return CacheMetrics {..}+  where+    c x = newStatCounter s ("fleet.cache." <> x) 10++-- | Add to a counter of the cache, if metrics are reported.+tally :: Member -> (CacheMetrics -> StatCounter) -> Int -> IO ()+tally m which n = forM_ m.metrics (\x -> incrementCounter (which x) n)++-- | A store that is a member of a fleet.+data Member = Member+  { config :: !MemberConfig,+    redis :: !R.Connection,+    keys :: !FleetKeys,+    db :: !DatabaseReader,+    -- | the database as it was at each recent block+    views :: !(Views BlockHash),+    -- | the blocks that views were taken of, newest first+    recent :: !(TVar [BlockHash]),+    -- | the blocks of the last report that was taken+    reported :: !(TVar [BlockHash]),+    -- | the fleet tip, read after that report+    tip :: !(TVar (Maybe BlockHash)),+    -- | the transactions of the store's mempool that the pool has+    offered :: !(TVar (HashSet TxHash)),+    -- | those that it refused, or that there was no time to offer, with+    -- the times at which they entered the mempool: they are offered again+    waiting :: !(TVar (HashMap TxHash UnixTime)),+    -- | how many times the mempool was offered, and at which tip last+    rounds :: !(TVar Int),+    offeredAt :: !(TVar (Maybe BlockHash)),+    -- | whether the best block changed since the mempool was last offered+    changed :: !(TVar Bool),+    -- | whether the store's own health checks pass. While they do not, the+    -- member does nothing, so that its lease runs out and a store that is+    -- stuck does not hold the fleet back.+    healthy :: !(TVar Bool),+    -- | the extended public keys that requests asked about and the cache+    -- did not have, by their names in Redis: 'memberCacheLoop' builds+    -- their entries+    wanted :: !(TVar (HashMap ByteString XPubSpec)),+    -- | the fleet tip at which the cache was found at another block, at+    -- the last turn+    lagging :: !(TVar (Maybe ByteString)),+    -- | the metrics of the cache, if the configuration has a StatsD client+    metrics :: !(Maybe CacheMetrics),+    -- | transactions that the store deleted from its mempool since it+    -- started, up to a hundred thousand. The database keeps nothing of+    -- them, and the rule for conflicts in the pool asks about them+    -- ('offerMempool').+    gone :: !(TVar (HashSet TxHash))+  }++-- | A member with no views yet, on a connection to the Redis of the fleet.+newMember :: MemberConfig -> R.Connection -> DatabaseReader -> IO Member+newMember config redis db = do+  views <- newViews db+  recent <- newTVarIO []+  reported <- newTVarIO []+  tip <- newTVarIO Nothing+  offered <- newTVarIO HashSet.empty+  waiting <- newTVarIO HashMap.empty+  rounds <- newTVarIO 0+  offeredAt <- newTVarIO Nothing+  changed <- newTVarIO True+  healthy <- newTVarIO True+  wanted <- newTVarIO HashMap.empty+  lagging <- newTVarIO Nothing+  metrics <- mapM newCacheMetrics config.stats+  gone <- newTVarIO HashSet.empty+  return Member {keys = fleetKeys config.prefix 32, ..}++-- | Take a view of the database for its best block. The block store calls+-- this right after it imports or reverts a block, and when it starts,+-- before it writes anything else: the view is then of that block and of+-- nothing more.+noteBest :: Member -> BlockHash -> IO ()+noteBest m block = do+  takeView m.views block+  atomically $ do+    modifyTVar' m.recent $ \blocks -> take m.config.depth (block : filter (/= block) blocks)+    writeTVar m.changed True+  prune m++-- | Note that transactions left the store's mempool unconfirmed. The store+-- calls this when it deletes them. The member forgets that it offered them:+-- one that comes back, as a transaction does that is sent again after it+-- was replaced, is then offered again, and takes the place of what replaced+-- it if this store's rules say so.+memberForget :: Member -> [TxHash] -> IO ()+memberForget m hs = atomically $ do+  modifyTVar' m.offered (\have -> foldr HashSet.delete have hs)+  modifyTVar' m.waiting (\held -> foldr HashMap.delete held hs)+  modifyTVar' m.gone $ \seen ->+    let more = foldr HashSet.insert seen hs+     in if HashSet.size more > 100000 then HashSet.fromList hs else more++-- | Let go of the views that the rules do not ask for: all but those of+-- the recent blocks, of the last report, and of the tip read after it.+prune :: Member -> IO ()+prune m = do+  recent <- readTVarIO m.recent+  reported <- readTVarIO m.reported+  tip <- readTVarIO m.tip+  keepViews m.views (nub (F.keep (recent <> reported) tip))++-- | Whether Redis answers a ping within two seconds.+memberAlive :: Member -> IO Bool+memberAlive m =+  handleAny (\_ -> return False) $+    timeout 2000000 (R.runRedis m.redis R.ping) <&> \case+      Just (Right _) -> True+      _ -> False++-- | One round of what a member does: report, move the tip if the stores+-- agree on another block, offer what the pool does not have of the store's+-- mempool, and let go of old views. Call it every second or two, and when+-- the best block changes. A store whose own health checks fail must not+-- call it: its lease then runs out, and the fleet goes on without it.+memberTick :: Member -> IO ()+memberTick m = do+  report m+  moved <- moveTip m+  -- the tip is read after the report, as the rule of 'F.keep' asks+  tip <- fmap decoded <$> fleetTip m.redis m.keys+  atomically (writeTVar m.tip (fromMaybe Nothing tip))+  prune m+  syncCache m+  unless moved (expirePool m)+  offerMempool m+  where+    decoded = either (const Nothing) Just . decode++-- | Take a turn as a member every two seconds, and at once when an action+-- that waits for the next best block returns, for as long as the store's+-- health checks pass. A turn that fails, as when Redis is away, is told to+-- a function and tried again at the next.+memberLoop :: Member -> IO () -> (String -> IO ()) -> IO a+memberLoop m nextBlock complain = go+  where+    go = do+      void (timeout 2000000 nextBlock)+      well <- readTVarIO m.healthy+      when well (handleAny (complain . show) (memberTick m))+      go++-- | Hear what happens to the fleet, for as long as the store runs: each+-- move of the tip and each transaction that enters or leaves the pool is+-- told to a function, in the order in which Redis has them. Every store of+-- the fleet hears the same, which is what its clients are told+-- ('FleetEvent').+--+-- The telling starts from now, or from the oldest event that Redis keeps:+-- the second is for a listener that wants to hold the same history as the+-- other stores whenever it started, as the log of events of the web server+-- does. When Redis cannot be read a function is told why, and listening+-- goes on from the last event heard, so nothing is left out unless Redis+-- dropped it meanwhile.+memberEvents :: Member -> Bool -> (FleetEvent -> IO ()) -> (String -> IO ()) -> IO a+memberEvents m whole tell complain = newIORef (if whole then Just "0-0" else Nothing) >>= start+  where+    start at = do+      handleAny (complain . show) $ do+        from <- readIORef at >>= maybe (fleetEventsEnd m.redis m.keys) return+        writeIORef at (Just from)+        listen at from+      void (timeout 1000000 (atomically retrySTM) :: IO (Maybe ()))+      start at+    listen at from = do+      events <- fleetEventsAfter m.redis m.keys from 1000+      mapM_ (tell . snd) events+      let next = foldl (\_ (place, _) -> place) from events+      writeIORef at (Just next)+      listen at next++-- | Report the blocks of the main chain that the store has views of, newest+-- first.+report :: Member -> IO ()+report m = do+  held <- heldViews m.views+  recent <- readTVarIO m.recent+  blocks <- filterM main (filter (`elem` held) recent)+  taken <- fleetReport m.redis m.keys Nothing m.config.lease m.config.name (map encode blocks) (map encode held)+  atomically (writeTVar m.reported (if taken then blocks else []))+  where+    main h = maybe False (.main) <$> Q.getBlock m.db h++-- | Move the fleet tip to the block that the live stores agree on, if it is+-- another one, with what the blocks in between did to the pool. Says+-- whether it moved. When this store cannot say what they did, the pool is+-- emptied, which Redis allows only to a store that is alone.+moveTip :: Member -> IO Bool+moveTip m = do+  seen <- fleetTip m.redis m.keys+  leases <- fleetLive m.redis m.keys+  recents <- redis' (R.hgetall m.keys.recent)+  now <- redis' R.time <&> \(s, us) -> fromIntegral (s * 1000 + us `div` 1000)+  let reports =+        Map.fromList+          [ (store, F.Report {recent = chunks blocks, expires})+          | (store, expires) <- leases,+            Just blocks <- [lookup store recents]+          ]+      fleet = F.Fleet {tip = seen, reports}+  case F.agreed now fleet of+    Just target | Just target /= seen -> do+      between <- case (traverse decode seen, decode target) of+        (Right from, Right to) -> blocksBetween m.db from to+        _ -> return Nothing+      let path = maybe [] (map encode . (.path)) between+      fleetAdvance m.redis m.keys 32 Nothing seen target ((.pool) <$> between) path >>= \case+        MovedTo _ -> advanceCache m seen target between >> return True+        _ -> return False+    _ -> return False+  where+    redis' :: R.Redis (Either R.Reply a) -> IO a+    redis' f = R.runRedis m.redis f >>= either (fail . ("Redis error: " <>) . show) return+    chunks bytes+      | B.null bytes = []+      | otherwise = B.take 32 bytes : chunks (B.drop 32 bytes)++-- | The most blocks that 'changeBetween' goes through on either side of a+-- fork. Beyond that, a store that is alone starts the pool again, which+-- costs the stores an offer of their mempools; going through a thousand+-- blocks would cost a read of every transaction in them.+farthest :: Int+farthest = 50++-- | What the blocks between two tips did, as far as the pool is concerned,+-- read from the database as it is now: the transactions of the blocks that+-- lead from the first tip, or from the genesis block, to where the main+-- chain forked from it, which come back or are gone, and those of the+-- blocks of the main chain from there to the second tip, which are+-- confirmed, with the outputs that they spend.+--+-- Without a first tip there was no pool, and the blocks did nothing to it.+-- 'Nothing' if the database does not have one of the tips, if the second+-- is not on its main chain, or if the way is longer than 'farthest'+-- blocks on either side.+changeBetween :: DatabaseReader -> Maybe BlockHash -> BlockHash -> IO (Maybe PoolChange)+changeBetween db from to = fmap (.pool) <$> blocksBetween db from to++-- | What the blocks between two tips did.+data Between = Between+  { -- | what they did to the pool+    pool :: !PoolChange,+    -- | the transactions of the blocks of the main chain from where it+    -- left the way to the first tip, up to the second, in their order+    added :: ![TxData],+    -- | whether blocks of the way to the first tip were abandoned+    reorganised :: !Bool,+    -- | those blocks of the main chain, oldest first, the second tip last:+    -- what the fleet tells its clients that the tip moved through+    path :: ![BlockHash]+  }++-- | 'changeBetween', with the transactions that the new blocks confirm,+-- which is what the cache of extended public keys is brought to the new+-- tip with ('advanceCache').+blocksBetween :: DatabaseReader -> Maybe BlockHash -> BlockHash -> IO (Maybe Between)+blocksBetween db from to = L.withView db $ \view -> do+  let r = L.viewReader view+      -- the blocks from one that is not on the main chain back to it+      back :: Int -> BlockHash -> IO (Maybe ([BlockData], BlockData))+      back n h+        | n > farthest = return Nothing+        | otherwise =+            Q.getBlock r h >>= \case+              Nothing -> return Nothing+              Just bd+                | bd.main -> return (Just ([], bd))+                | otherwise -> fmap (\(bs, fork) -> (bd : bs, fork)) <$> back (n + 1) bd.header.prev+      -- the blocks of the main chain from a block down to above a height+      down :: Int -> BlockData -> BlockData -> IO (Maybe [BlockData])+      down n fork bd+        | n > farthest = return Nothing+        | bd.height <= fork.height = return (if bd.header == fork.header then Just [] else Nothing)+        | otherwise =+            Q.getBlock r bd.header.prev >>= \case+              Nothing -> return Nothing+              Just parent -> fmap (bd :) <$> down (n + 1) fork parent+  target <- Q.getBlock r to+  left <- case from of+    Just h -> back 0 h+    -- without a tip there was no pool, and nothing for the blocks to do+    Nothing -> return (fmap ([],) target)+  case (target, left) of+    (Just bd, Just (abandoned, fork))+      | bd.main ->+          down 0 fork bd >>= \case+            Nothing -> return Nothing+            Just added -> do+              old <- concat <$> mapM (bodies view) (reverse abandoned)+              new <- concat <$> mapM (confirmedBy r) (reverse added)+              let pool =+                    PoolChange+                      { returned = [poolTx r.net r.ctx td | (_, Just td) <- old, not (coinbase td)],+                        gone = [encode h | (h, found) <- old, maybe True coinbase found],+                        confirmed = map (encode . txHash . (.tx)) new,+                        spent = [encode i.outpoint | td <- new, i <- td.tx.inputs, i.outpoint /= nullOutPoint]+                      }+              let path = map (headerHash . (.header)) (reverse added)+              return (Just Between {pool, added = new, reorganised = not (null abandoned), path})+    _ -> return Nothing+  where+    -- the transactions of an abandoned block, each with what the store has+    -- of it now, if it still has it and did not delete it+    bodies view bd = do+      found <- L.getManyTxData view bd.txs+      return [(h, found' >>= live) | (h, found') <- zip bd.txs found]+    live td = if td.deleted then Nothing else Just td+    confirmedBy :: DatabaseReader -> BlockData -> IO [TxData]+    confirmedBy r bd = catMaybes <$> Q.retrieveManyCF r.db (txCF r.db) (map TxKey bd.txs)+    coinbase td = any ((== nullOutPoint) . (.outpoint)) td.tx.inputs++-- | A transaction as the pool keeps it: its body without spenders, and+-- what it does at each address, as "Haskoin.Store.Database.Mempool" works+-- them out for the store's own mempool area.+poolTx :: Network -> Ctx -> TxData -> PoolTx+poolTx net ctx td =+  PoolTx+    { txid = encode records.txid,+      time,+      inputs = [encode op | (op, _) <- records.spends],+      body = encode records.body,+      signals = net.replaceByFee && any ((< 0xffffffff - 1) . (.sequence)) td.tx.inputs,+      entries = mapMaybe entry records.addresses+    }+  where+    time = case td.block of+      MemRef t -> t+      BlockRef {} -> 0+    records = M.mempoolRecords net ctx time td+    entry = \case+      (MemAddrTxKey a _ _, _) -> Just (PoolTouches (encode a))+      (MemAddrSpentKey a p, MemAddrSpentVal v) -> Just (PoolSpends (encode a) (encode p) v)+      (MemAddrOutKey a _ p, MemAddrOutVal v s) -> Just (PoolPays (encode a) (encode p) v s)+      _ -> Nothing++-- | Offer to the pool the transactions of the store's mempool that it does+-- not have, oldest first, so that each comes after those that it spends+-- from. A store without a view of the fleet tip offers nothing: it cannot+-- say what is unspent there.+--+-- A transaction that the pool refuses is offered again at the next round:+-- its parents may not be there yet, or the tip may have moved. One that+-- conflicts with transactions of the pool takes their place when this+-- store deleted every one of them by its own rules, as it does when it+-- replaces a transaction, and otherwise stays out: the first of two that+-- spend the same output stays.+--+-- = Reading no more of the mempool than is new+--+-- A mempool has tens of thousands of transactions and a round comes every+-- two seconds, so a round does not read the whole list. It reads the list+-- from the newest, a page at a time, until a page has nothing that it does+-- not know, and offers what it found with what is waiting from earlier+-- rounds. The whole list is read when the tip has moved or the store's best+-- block has changed, since a block or a reorganisation changes the mempool+-- anywhere, and every 'wholeEvery' rounds, for whatever the short way+-- missed. What the store deletes in between it tells the member+-- ('memberForget'). A round offers 'mostOffers'+-- transactions at most and leaves the rest waiting, so that a store with a+-- large mempool to offer still reports in time to keep its lease.+offerMempool :: Member -> IO ()+offerMempool m =+  readTVarIO m.tip >>= \case+    Nothing -> return ()+    Just tip -> void . withViewAt m.views tip $ \at -> do+      before <- readTVarIO m.offeredAt+      (n, dirty) <- atomically $ do+        writeTVar m.offeredAt (Just tip)+        (,) <$> stateTVar m.rounds (\k -> (k, k + 1)) <*> stateTVar m.changed (,False)+      todo <- if dirty || before /= Just tip || n `mod` wholeEvery == 0 then everything else newest+      let (now, later) = splitAt mostOffers (sortOn fst todo)+      atomically (modifyTVar' m.waiting (HashMap.union (HashMap.fromList [(h, time) | (time, h) <- later])))+      go at tip now+  where+    -- all of the mempool that the pool does not have, forgetting what has+    -- left the mempool+    everything = do+      mempool <- Q.getMempool m.db+      have <- readTVarIO m.offered+      let kept = HashSet.intersection have (HashSet.fromList (map snd mempool))+      atomically (writeTVar m.offered kept >> writeTVar m.waiting HashMap.empty)+      return [(time, h) | (time, h) <- mempool, not (HashSet.member h kept)]+    -- the newest of the mempool that this member has not seen, and what is+    -- waiting+    newest = do+      have <- readTVarIO m.offered+      held <- readTVarIO m.waiting+      let known h = HashSet.member h have || HashMap.member h held+          scan offset = do+            page <- Q.getMempoolPage m.db Limits {limit = pageSize, offset, start = Nothing}+            let fresh = filter (not . known . snd) page+            if null fresh || length page < fromIntegral pageSize+              then return fresh+              else (fresh <>) <$> scan (offset + pageSize)+      found <- scan 0+      return (found <> [(time, h) | (h, time) <- HashMap.toList held])+    pageSize = 256+    go _ _ [] = return ()+    go at tip ((time, h) : rest) =+      M.getMempoolTx m.db h >>= \case+        Nothing -> forget h >> go at tip rest+        Just td -> do+          let tx = poolTx m.db.net m.db.ctx td+          unspent <- forM [i.outpoint | i <- td.tx.inputs, i.outpoint /= nullOutPoint] $ \p ->+            isJust <$> Q.getUnspent at p+          let offer = poolOffer m.redis m.keys 32 (Just (encode tip)) tx unspent+              done = atomically $ do+                modifyTVar' m.offered (HashSet.insert h)+                modifyTVar' m.waiting (HashMap.delete h)+              wait = atomically (modifyTVar' m.waiting (HashMap.insert h time))+          offer [] >>= \case+            Taken -> done >> go at tip rest+            Known -> done >> go at tip rest+            Orphan _ -> wait >> go at tip rest+            -- the tip moved: the next round reads the whole mempool+            Late -> return ()+            Conflict rivals -> do+              mine <- mapM deletedHere rivals+              if and mine+                then+                  offer rivals >>= \case+                    Taken -> done+                    _ -> wait+                else wait+              go at tip rest+    forget h = atomically (modifyTVar' m.waiting (HashMap.delete h))+    deletedHere rival = case decode rival of+      Left _ -> return False+      -- it deleted it, and has not taken it back since. A store that was+      -- started again does not remember, and leaves the conflict as it+      -- is: the first of the two stays in the pool.+      Right h -> do+        deleted <- HashSet.member h <$> readTVarIO m.gone+        back <- isJust <$> M.getMempoolTx m.db h+        return (deleted && not back)++-- | How many rounds of offers go by between two that read the whole+-- mempool: five minutes of them.+wholeEvery :: Int+wholeEvery = 150++-- | The most transactions that one round offers. An offer takes a tenth of+-- a millisecond or two, so this is well under the two seconds of a round.+mostOffers :: Int+mostOffers = 2000++-- | Take out of the pool the transactions that entered a mempool longer+-- ago than the configuration allows, with what spends them.+expirePool :: Member -> IO ()+expirePool m = when (m.config.expiry > 0) $ do+  (now, _) <- R.runRedis m.redis R.time >>= either (fail . show) return+  let before = fromIntegral now - fromIntegral m.config.expiry :: Double+  old <-+    R.runRedis m.redis (R.zrangebyscoreLimit (m.keys.prefix <> "pool:time") 0 before 0 1000)+      >>= either (fail . show) return+  unless (null old) (void (poolWithdraw m.redis m.keys 32 old))++-- | Where a store stands when a request comes.+data Standing a+  = -- | the request was answered from the view at the fleet tip+    Answered !a+  | -- | there is no fleet tip yet: the store answers by itself+    Alone+  | -- | the store has no view of the fleet tip, so any answer of its own+    -- would differ from those of the other stores+    Behind++-- | What a read of the pool throws when the fleet tip is no longer the+-- block that the request answers from. The request is then answered again,+-- from the new tip.+data TipMoved = TipMoved+  deriving (Show)++instance Exception TipMoved++-- | Answer a request as a member of a fleet: read the fleet tip, and run+-- an action with the database as it was at that block and the pool as it is+-- at that tip. The action must do all its reading before it returns.+answering :: Member -> (DatabaseReader -> PoolReads -> IO a) -> IO (Standing a)+answering m action =+  memberTip m >>= \case+    Nothing -> return Alone+    Just tip -> maybe Behind Answered <$> answeringAt m tip (\r pool _ -> action r pool)++-- | The fleet tip as Redis has it now, if there is one.+memberTip :: Member -> IO (Maybe ByteString)+memberTip m = fleetTip m.redis m.keys++-- | Run an action with the database as it was at a block that is taken to+-- be the fleet tip, and the pool as it is at that tip, or give 'Nothing' if+-- the store has no view of that block. The action must do all its reading+-- before it returns.+--+-- The block need not be the tip any more: it can be the one that the+-- member read at its last turn, which saves a request the round trip of+-- asking. Every read of the pool checks it and throws v'TipMoved' if the tip+-- is another one. The action is also given a check to run when it has what+-- it needs: it asks Redis for the tip if no read of the pool has done so,+-- and throws the same. An answer that passed either is one that every+-- store of the fleet gave at that moment.+answeringAt :: Member -> ByteString -> (DatabaseReader -> PoolReads -> IO () -> IO a) -> IO (Maybe a)+answeringAt m tip action = answeringWith m tip (\r pool _ confirm -> action r pool confirm)++-- | 'answeringAt', for an action that reads Redis by other ways than the+-- pool too: it is also given what to run after each such read that found+-- the tip where it should be.+answeringWith :: Member -> ByteString -> (DatabaseReader -> PoolReads -> IO () -> IO () -> IO a) -> IO (Maybe a)+answeringWith m tip action = case decode tip of+  Left _ -> return Nothing+  Right block -> withViewAt m.views block $ \r -> do+    checked <- newIORef False+    let confirm =+          readIORef checked >>= \case+            True -> return ()+            False -> do+              now <- memberTip m+              if now == Just tip then writeIORef checked True else throwIO TipMoved+        found = writeIORef checked True+    action r (redisPool m.redis m.keys tip found) found confirm++-- | 'answeringAt' with the queries ready: those of+-- "Haskoin.Store.Database.Query.Pooled" for the view and the pool, and+-- the same with the cache of extended public keys in front+-- ('cachedRead'), which is what requests are answered with unless they+-- ask not to be.+answeringCached :: Member -> ByteString -> (StoreRead -> StoreRead -> IO () -> IO a) -> IO (Maybe a)+answeringCached m tip action = answeringWith m tip $ \r pool found confirm -> do+  cached <- cachedRead m tip r pool found+  action (pooledRead r pool) cached confirm++-------------------------------------------------------------------------------+-- The cache of extended public keys+-------------------------------------------------------------------------------++-- | The queries of a request with the fleet's cache of extended public+-- keys in front of those about them: given the tip that the request+-- answers from, the view at that block, the pool, and what to run after a+-- read that found the tip where it should be.+--+-- The balances of a key come from the cache if it has the key with the+-- addresses that it has now. The other three queries about a key are given+-- its balances by the caller, and are answered from the cache when this+-- same request got them there: the cache is then asked for an entry with+-- as many addresses, so that an entry that changed in between is not+-- used. Whatever the cache does not answer the database does, and a key+-- that it did not have is put down to be built ('wantCached'). A read that+-- finds the tip moved throws v'TipMoved', like those of the pool.+cachedRead :: Member -> ByteString -> DatabaseReader -> PoolReads -> IO () -> IO StoreRead+cachedRead m tip r pool found+  | m.config.cacheMax <= 0 = return base+  | otherwise = do+      seen <- newIORef HashMap.empty+      let hit :: Cached a -> IO (Maybe a)+          hit = \case+            Hit x -> Just x <$ found+            Miss -> return Nothing+            Moved -> throwIO TipMoved+          entryOf x = HashMap.lookup (xpubId x) <$> readIORef seen+          -- from the cache if this request has the entry of the key, and+          -- from where the page starts if it has a start+          paged :: XPubSpec -> Limits -> IO [a] -> (Entry -> L.From -> IO (Cached [a])) -> IO [a]+          paged x limits direct cached =+            entryOf x >>= \case+              Nothing -> direct+              Just e ->+                pageStart r pool limits.start >>= \case+                  Nothing -> return []+                  Just from -> cached e from >>= hit >>= maybe direct return+      return+        base+          { xPubBals = \x -> do+              now <- milliseconds+              cachedEntry m.redis m.keys tip r.initGap r.maxGap now x >>= hit >>= \case+                Just e -> do+                  tally m (.hits) 1+                  e.balances <$ modifyIORef' seen (HashMap.insert (xpubId x) e)+                Nothing -> do+                  tally m (.misses) 1+                  bals <- base.xPubBals x+                  wantCached m x bals+                  return bals,+            xPubTxs = \x bals limits ->+              paged x limits (base.xPubTxs x bals limits) $ \e from ->+                cachedTxs m.redis m.keys tip x e.sizes from limits,+            xPubUnspents = \x bals limits ->+              paged x limits (base.xPubUnspents x bals limits) $ \e from ->+                cachedUnspents m.redis m.keys tip x e from limits,+            xPubTxCount = \x bals ->+              entryOf x >>= \case+                Nothing -> base.xPubTxCount x bals+                Just e ->+                  cachedCount m.redis m.keys tip x e.sizes >>= hit >>= maybe (base.xPubTxCount x bals) return+          }+  where+    base = pooledRead r pool++-- | The time in milliseconds, by which the cache orders the keys by use.+milliseconds :: IO Word64+milliseconds = round . (* 1000) <$> getPOSIXTime++-- | Put an extended public key down to be cached, given its balances, if+-- it is worth it: a key with few addresses and few transactions is read+-- from the database as fast as from Redis. The thresholds are those of the+-- configuration, and count as "Haskoin.Store.Cache" counts them.+wantCached :: Member -> XPubSpec -> [XPubBal] -> IO ()+wantCached m x bals =+  when (m.config.cacheMax > 0 && (length used >= m.config.cacheMinAddrs || txs >= m.config.cacheMinTxs)) $+    atomically . modifyTVar' m.wanted $ \w ->+      if HashMap.size w < 1000 then HashMap.insert (xpubId x) x w else w+  where+    used = filter (not . nullBalance . (.balance)) bals+    txs = fromIntegral (sum (map (.balance.txs) used)) :: Int++-- | Build the entries of the keys that requests asked about and the cache+-- did not have, one at a time, for as long as the store runs. A build that+-- fails is told to a function. The least recently used entries are deleted+-- when there are more than the configuration allows.+--+-- This is apart from the turns of the member because an entry of a wallet+-- with a long history takes seconds to read from the database, and a+-- member that does not report for that long loses its lease.+memberCacheLoop :: Member -> (String -> IO ()) -> IO a+memberCacheLoop m complain = go+  where+    go = do+      spec <- atomically $ do+        w <- readTVar m.wanted+        case HashMap.toList w of+          [] -> retrySTM+          (k, x) : _ -> x <$ writeTVar m.wanted (HashMap.delete k w)+      handleAny (complain . show) (build spec)+      go+    build spec =+      readTVarIO m.tip >>= \case+        Nothing -> return ()+        Just block -> do+          now <- milliseconds+          let tip = encode block+              name = decodeUtf8 (xpubId spec)+          built <-+            handle (\TipMoved -> return (Just Abandoned)) $+              answeringAt m tip (\r pool _ -> cacheBuild m.redis m.keys tip r pool now spec)+          took <- subtract now <$> milliseconds+          case built of+            Just (Built n) | n > 0 -> do+              tally m (.builds) 1+              forM_ m.metrics (\x -> addTiming x.buildTime (fromIntegral took))+              m.config.note LevelDebug $+                "Cached xpub " <> name <> ": " <> shown n <> " addresses added in " <> shown took <> " ms"+              gone <- cacheEvict m.redis m.keys m.config.cacheMax+              when (gone > 0) $ do+                tally m (.evictions) gone+                m.config.note LevelDebug ("Evicted " <> shown gone <> " xpubs from the cache")+            Just Abandoned -> do+              tally m (.abandoned) 1+              m.config.note LevelDebug ("Gave up caching xpub " <> name <> ": the fleet or the entry moved on")+            _ -> return ()++-- | Bring the cache of extended public keys to the block that this member+-- just moved the fleet tip to, with what the blocks in between did at the+-- addresses that its entries have. The cache is emptied instead when the+-- member cannot say what they did, when the chain was reorganised, which+-- is rare and would need what the abandoned blocks did undone, or when+-- the member has no view of the block.+advanceCache :: Member -> Maybe ByteString -> ByteString -> Maybe Between -> IO ()+advanceCache m seen target between = when (m.config.cacheMax > 0) $ do+  changes <- case (between, decode target) of+    (Just b, Right to) | not b.reorganised -> withViewAt m.views to $ \r -> do+      let touched = HashMap.toList (blockChanges r.net r.ctx b.added)+      flags <- cacheTracked m.redis m.keys (map fst touched)+      forM [x | (x, True) <- zip touched flags] $ \(a, change) -> do+        bal <- Q.getBalance r a+        return (a, bal, change)+    _ -> return Nothing+  let why = case (between, changes) of+        (Nothing, _) -> "this store cannot say what the blocks since the last tip did"+        (Just b, _) | b.reorganised -> "the chain was reorganised"+        (_, Nothing) -> "this store has no view of the block"+        _ -> "it was at another block than the last tip"+  cacheAdvance m.redis m.keys seen target changes >>= \case+    CacheEmptied -> wasEmptied m target why+    CacheMoved ->+      m.config.note LevelDebug $+        "The xpub cache is at block "+          <> blockName target+          <> ": "+          <> shown (maybe 0 length changes)+          <> " addresses changed"+    _ -> return ()++-- | Count and say that this store emptied the cache, at a block and for a+-- reason. It is information and not a warning: nothing is wrong with what+-- the fleet answers, which comes from the database until entries are+-- built again. Emptied at every block, though, the cache is of no use,+-- and the log is where that shows.+wasEmptied :: Member -> ByteString -> Text -> IO ()+wasEmptied m block why = do+  tally m (.emptied) 1+  m.config.note LevelInfo ("Emptied the xpub cache at block " <> blockName block <> ": " <> why)++-- | A block as a log names it.+blockName :: ByteString -> Text+blockName bytes = either (const "?") blockHashToHex (decode bytes :: Either String BlockHash)++shown :: (Show a) => a -> Text+shown = T.pack . show++-- | See that the cache of extended public keys does not stay at another+-- block than the fleet tip. It is there for a moment after every move of+-- the tip, until the store that moved it has brought the cache along, and+-- for good if that store stopped before it did. So a member that finds+-- the cache behind the same tip at two turns in a row empties it, and it+-- starts again at the tip. Every turn of a member runs this.+syncCache :: Member -> IO ()+syncCache m = when (m.config.cacheMax > 0) $ do+  -- how large the cache is, at every turn: it is one small read, and the+  -- number is the fleet's, the same from every store+  forM_ m.metrics $ \x -> do+    (entries, held) <- cacheSize m.redis m.keys+    setGauge x.xpubs entries+    setGauge x.addresses held+  tip <- fmap encode <$> readTVarIO m.tip+  at <- cacheTip m.redis m.keys+  before <- readTVarIO m.lagging+  case tip of+    Just t | at /= Just t -> do+      when (before == Just t) $+        cacheAdvance m.redis m.keys Nothing t Nothing >>= \case+          CacheEmptied -> wasEmptied m t "no store brought it to the fleet tip"+          _ -> return ()+      atomically (writeTVar m.lagging (Just t))+    _ -> atomically (writeTVar m.lagging Nothing)++-- | The shared pool of a fleet as it is at a tip, for the queries of+-- "Haskoin.Store.Database.Query.Pooled". Every read throws v'TipMoved' if+-- the tip is another one by then, and runs an action if it is not, by which+-- a caller knows that the tip was checked.+redisPool :: R.Connection -> FleetKeys -> ByteString -> IO () -> PoolReads+redisPool conn keys tip checked =+  PoolReads+    { summaries = \as -> case as of+        [] -> return []+        _ -> map summary <$> ask "sums" (map encode as),+      addressTxs = \a start n -> do+        rows <- ask "txs" (encode a : from start <> [num n])+        return [(number time, h) | [member, time] <- groups 2 rows, Right h <- [decode member]],+      addressOuts = \a start n -> do+        rows <- ask "outs" (encode a : from start <> [num n])+        let paid time p v s = Unspent {block = MemRef time, outpoint = p, value = v, script = s, address = Just a}+        return+          [ (paid (number time) p (number v) s, f == "1")+          | [member, time, kept, f] <- groups 4 rows,+            Right p <- [decode member],+            let (v, s) = B.drop 1 <$> C.break (== ' ') kept+          ],+      spentOutputs = \a ps -> case ps of+        [] -> return []+        _ -> map (== '1') . C.unpack . B.concat <$> ask "spent" [encode a, B.concat (map encode ps)],+      bodies = \hs -> case hs of+        [] -> return []+        _ -> transactions <$> ask "bodies" [B.concat (map encode hs)],+      spenders = \ps -> case ps of+        [] -> return []+        _ -> do+          rows <- ask "spenders" [B.concat (map encode ps)]+          return [spender t i | [t, i] <- groups 2 rows],+      page = \offset limit -> do+        rows <- ask "page" [num offset, num limit]+        return [(number time, h) | [member, time] <- groups 2 rows, Right h <- [decode member]],+      startingWith = \start -> rights . map decode <$> ask "ids" [B.drop 1 (encode (TxKeyS start))]+    }+  where+    ask what args = poolRead conn keys 32 (Just tip) what args >>= maybe (throwIO TipMoved) (<$ checked)+    num :: (Show a) => a -> ByteString+    num = C.pack . show+    number :: (Num a) => ByteString -> a+    number = maybe 0 (fromInteger . fst) . C.readInteger+    from :: (Serialize k) => Maybe (UnixTime, k) -> [ByteString]+    from = maybe ["", ""] (\(time, k) -> [num time, encode k])+    groups n rows+      | null rows = []+      | otherwise = take n rows : groups n (drop n rows)+    -- for each transaction: its body, whether it can be replaced by fee,+    -- how many of its outputs the pool spends, and those with what spends+    -- them+    transactions (bytes : flag : count : rest) =+      let (spent, more) = splitAt (3 * number count) rest+          inputs =+            IntMap.fromList+              [ (fromIntegral p.index, s)+              | [o, t, i] <- groups 3 spent,+                Right p <- [outpoint o],+                Just s <- [spender t i]+              ]+       in (body bytes (flag == "1"), inputs) : transactions more+    transactions _ = []+    outpoint :: ByteString -> Either String OutPoint+    outpoint = decode+    summary row = case map number (C.words row) of+      [txs, paid, paidCount, spent, spentCount, both, bothCount] -> Just MemSummary {..}+      _ -> Nothing+    -- whether a transaction can be replaced by fee is what the pool says+    -- now, not what the store that offered it found then+    body bytes can+      | B.null bytes = Nothing+      | otherwise = either (const Nothing) (\td -> Just (td :: TxData) {rbf = can}) (decode bytes)+    spender t i+      | B.null t = Nothing+      | otherwise = either (const Nothing) (\h -> Just Spender {txid = h, index = number i}) (decode t)
+ src/Haskoin/Store/Fleet/Pool.hs view
@@ -0,0 +1,320 @@+-- |+-- 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)
+ src/Haskoin/Store/Fleet/Redis.hs view
@@ -0,0 +1,400 @@+{-# 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)
+ src/Haskoin/Store/Fleet/XPub.hs view
@@ -0,0 +1,642 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module      : Haskoin.Store.Fleet.XPub+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The cache of extended public keys of a fleet of stores: what a store that+-- answers as a member of a fleet ("Haskoin.Store.Fleet.Member") keeps in+-- the Redis that the fleet shares, so that a request about a wallet is one+-- or two reads of Redis instead of a read of the database for each of its+-- addresses. It is built on the shared pool, and described in "The cache+-- of extended public keys of a fleet" in @docs\/layered-database.md@.+--+-- = What an entry is+--+-- An entry belongs to one extended public key and has, for the addresses+-- that a scan of the key reads ('Haskoin.Store.Common.scanXPubBals'):+--+-- * __the chain's layer__: the balance of each address in the chain, the+--   transactions of all of them, and their unspent outputs, as they are at+--   one block, which is the same for every entry and is written next to+--   them;+--+-- * __the pool's layer__: the transactions of the shared pool that touch+--   those addresses and the outputs that it pays to them. The script of the+--   pool puts them in and takes them out in the step in which it changes+--   the pool, so this layer is the pool, always.+--+-- Transactions and outputs of both layers are in one list each, ordered as+-- the API orders them, so a page is a range of a sorted set. A balance is+-- the chain's with the summary that the pool keeps for the address.+--+-- = Why it cannot give another answer than the database+--+-- A store of a fleet answers from the chain as it was at the fleet tip+-- with the pool on top ("Haskoin.Store.Database.Query.Pooled"). An entry is+-- that same answer worked out beforehand, and it is used only while that+-- holds:+--+-- * __The chain's layer follows from the chain__ at a block and the+--   addresses. Any store computes the same, so it does not matter which one+--   writes, and there is nothing for stores to disagree on.+--+-- * __A read answers only where the cache is__, which is a move of the+--   fleet tip and the block that it went to, and only while the tip is+--   there. A block alone would not do: the tip can leave a block and come+--   back to it, as when a block is reverted and mined again, and on the+--   way the pool takes that block's transactions out of the entries and+--   puts them back as unconfirmed. When the tip moves, the store that moved it+--   brings the cache there, with what the blocks in between did to the+--   addresses that entries have ('cacheAdvance'). Until it has, every read+--   is a miss and the store answers from its database. If it never does, or+--   the chain was reorganised, or the pool was emptied, the cache is+--   emptied: the next move finds it at a block that it does not start from.+--+-- * __The addresses of a key depend on which are used__, and the pool+--   counts. So every read of balances runs the scan again over what the+--   entry has ('window'), and is a miss unless the scan reads exactly the+--   addresses of the entry. The other reads say how many addresses the+--   caller found, and miss if the entry has another number.+--+-- The alternative that this replaces, "Haskoin.Store.Cache", keeps one+-- store's view of each key and repairs it from that store's events. It is+-- still what a store that is not in a fleet uses.+--+-- = What it costs+--+-- An entry is built from the database by the store that missed, in the+-- background, a few addresses at a time ('cacheBuild'); when a key gains+-- addresses only those are read. Each block costs one question to Redis+-- about the addresses that it touches, and a write for those that entries+-- have.+module Haskoin.Store.Fleet.XPub+  ( -- * Reading+    Cached (..),+    Entry (..),+    cachedEntry,+    cachedTxs,+    cachedUnspents,+    cachedCount,+    window,++    -- * Writing+    Built (..),+    cacheBuild,+    cacheDrop,+    cacheHave,+    CacheMoved (..),+    AddressChange (..),+    blockChanges,+    cacheTracked,+    cacheAdvance,+    cacheEvict,+    cacheTip,+    cacheSize,++    -- * Encoding+    xpubId,+    score,+    position,+    nothingPool,+  )+where++import Control.Monad (forM)+import Data.Bits (shift, (.&.), (.|.))+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Char8 qualified as C+import Data.Default (def)+import Data.FileEmbed (embedFile, makeRelativeToProject)+import Data.Functor ((<&>))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Int (Int64)+import Data.IntMap.Strict qualified as IntMap+import Data.List (sortOn)+import Data.Maybe (fromMaybe, mapMaybe)+import Data.Serialize (decode, encode, runPut)+import Data.Serialize.Put (putWord32be, putWord8)+import Data.Text.Encoding (encodeUtf8)+import Data.Word (Word32, Word64)+import Database.Redis (Connection, Redis, Reply)+import Database.Redis qualified as R+import Haskoin+  ( Address (..),+    Ctx,+    Network,+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    addressHash,+    encodeHex,+    nullOutPoint,+    scriptToAddressBS,+    sha256,+    txHash,+  )+import Haskoin.Store.Common (Limits (..), StoreRead (..), scanXPubBals)+import Haskoin.Store.Data+  ( Balance (..),+    BlockRef (..),+    Prev (..),+    TxData (..),+    TxRef (..),+    Unspent (..),+    XPubBal (..),+    XPubSpec (..),+    XPubUnspent (..),+    nullBalance,+    zeroBalance,+  )+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered (From (..))+import Haskoin.Store.Database.Query.Pooled (PoolReads (..), pooledRead)+import Haskoin.Store.Database.Reader (DatabaseReader (..))+import Haskoin.Store.Database.Types (MemSummary (..))+import Haskoin.Store.Fleet.Redis (FleetKeys (..))++-------------------------------------------------------------------------------+-- Encoding+-------------------------------------------------------------------------------++-- | What an extended public key is called in Redis: the start of a hash of+-- the key and of the kind of its addresses.+xpubId :: XPubSpec -> ByteString+xpubId = encodeUtf8 . encodeHex . B.take 16 . encode . sha256 . encode++-- | The score of a position in the sorted sets of an entry, such that+-- sorting by score puts the newest first, as the API lists transactions:+-- those of the pool have the negative of the time at which they entered+-- it, and those of the chain a positive number that goes down with height+-- and position in the block. Between equal scores Redis orders members by+-- their bytes, which for hashes and outputs is the order of the database+-- ('Haskoin.Store.Common.txRefKey', 'Haskoin.Store.Common.unspentKey').+--+-- It is the score of "Haskoin.Store.Cache", as a whole number: height and+-- position fit in the 53 bits that Redis keeps exactly.+score :: BlockRef -> Int64+score BlockRef {height = h, position = p} = 0x001fffffffffffff - (h' .|. p')+  where+    h' = (fromIntegral h .&. 0x07ffffff) `shift` 26+    p' = fromIntegral p .&. 0x03ffffff+score MemRef {timestamp = t} = negate (fromIntegral (t .&. 0x001fffffffffffff))++-- | The position that has a score: the inverse of 'score'.+position :: Int64 -> BlockRef+position s+  | s < 0 = MemRef {timestamp = fromIntegral (negate s)}+  | otherwise = BlockRef {height = fromIntegral (m `shift` (-26)), position = fromIntegral (m .&. 0x03ffffff)}+  where+    m = 0x001fffffffffffff - s++-- | A pool that has nothing. The queries of+-- "Haskoin.Store.Database.Query.Pooled" with it answer from the chain+-- alone, which is how the chain's layer of an entry is read.+nothingPool :: PoolReads+nothingPool =+  PoolReads+    { summaries = return . map (const Nothing),+      addressTxs = \_ _ _ -> return [],+      addressOuts = \_ _ _ -> return [],+      spentOutputs = \_ ps -> return (map (const False) ps),+      bodies = return . map (const (Nothing, IntMap.empty)),+      spenders = return . map (const Nothing),+      page = \_ _ -> return [],+      startingWith = \_ -> return []+    }++-------------------------------------------------------------------------------+-- Reading+-------------------------------------------------------------------------------++-- | What a read of the cache gave.+data Cached a+  = -- | the answer, which is the one that the database gives at the fleet+    -- tip with the pool on top+    Hit !a+  | -- | the cache cannot answer: ask the database+    Miss+  | -- | the fleet tip is another block than the one that the request+    -- answers from: answer again from the new tip+    Moved+  deriving (Eq, Show, Functor)++-- | The balances of a cached key, and what the other reads need of them.+data Entry = Entry+  { -- | the balances, as 'Haskoin.Store.Common.StoreRead' gives them+    balances :: ![XPubBal],+    -- | the number of addresses in each of the two chains of the key+    sizes :: !(Int, Int),+    -- | where each address is in the key. It is worked out when it is+    -- asked for, which only a list of unspent outputs does.+    paths :: HashMap Address [Word32]+  }+  deriving (Eq, Show)++-- | The balances of a key at the fleet tip, with the pool applied, if the+-- cache has the key with exactly the addresses that a scan of it reads+-- now. Arguments: the tip that the request answers from, the initial gap+-- and the gap of the store, and the time in milliseconds, by which the+-- cache knows which keys are in use.+cachedEntry :: Connection -> FleetKeys -> ByteString -> Word32 -> Word32 -> Word64 -> XPubSpec -> IO (Cached Entry)+cachedEntry conn keys tip igap gap now spec =+  ask conn keys "bals" [tip, xpubId spec, num now] <&> \case+    Hit rows -> maybe Miss Hit (entry (mapMaybe row (groups 3 rows)))+    Miss -> Miss+    Moved -> Moved+  where+    row [a, kept, sums] = do+      address <- either (const Nothing) Just (decode a)+      let (path, bal) = B.splitAt 5 kept+      [m, _, _, _, _] <- Just (B.unpack path)+      n <- either (const Nothing) Just (decode (B.drop 1 path)) :: Maybe Word32+      let stored = if B.null bal then Nothing else either (const Nothing) Just (decode bal)+          chain = fromMaybe (zeroBalance address) stored+      return (fromIntegral m, n, maybe chain (M.applySummary chain) (summary sums))+    row _ = Nothing+    summary sums = case map number (C.words sums) of+      [txs, paid, paidCount, spent, spentCount, both, bothCount] -> Just MemSummary {..}+      _ -> Nothing+    entry rows = do+      balances <- window igap gap rows+      let count m = length [() | XPubBal {path = m' : _} <- balances, m' == m]+      return+        Entry+          { balances,+            sizes = (count 0, count 1),+            paths = HashMap.fromList [(b.balance.address, b.path) | b <- balances]+          }++-- | The balances of a key as a scan of it gives them, from the balances of+-- the addresses that an entry has, each with the chain that it is in and+-- its number there: if the scan, run over them, reads exactly those+-- addresses. 'Nothing' if it would read one that the entry does not have,+-- or would stop before one that it has.+--+-- This is what makes an entry safe against the gap. The addresses of a key+-- are those up to the last one that is used and so many more, and a+-- payment in the pool counts as use. An entry is built for the addresses+-- of one moment; after a payment to one of its unused addresses the key+-- has more, and after such a payment is dropped it has fewer. Either way+-- this gives 'Nothing', and the store answers from the database and+-- builds the entry again.+window :: Word32 -> Word32 -> [(Word32, Word32, Balance)] -> Maybe [XPubBal]+window igap gap rows+  | not (contiguous ext && contiguous chg) = Nothing+  | otherwise = do+      found <- scanXPubBals (traverse (`HashMap.lookup` known)) igap gap (ext <> [beyond]) (chg <> [beyond])+      if length found == HashMap.size known then Just found else Nothing+  where+    chain m = sortOn fst [(n, b.address) | (m', n, b) <- rows, m' == m]+    ext = chain 0+    chg = chain 1+    contiguous as = map fst as == take (length as) [0 ..]+    known = HashMap.fromList [(b.address, b) | (_, _, b) <- rows]+    -- an address that the entry does not have, after the last of each+    -- chain: a scan that wants more than the entry has asks for it+    beyond = (maxBound, other 0)+    other :: Word32 -> Address+    other i =+      let a = PubKeyAddress (addressHash (encode i))+       in if HashMap.member a known then other (i + 1) else a++-- | A page of the transactions of a cached key, newest first, from where+-- the page starts, given the numbers of addresses that the caller has for+-- the key ('sizes').+cachedTxs :: Connection -> FleetKeys -> ByteString -> XPubSpec -> (Int, Int) -> From -> Limits -> IO (Cached [TxRef])+cachedTxs conn keys tip spec (n0, n1) from limits =+  ask conn keys "txs" (who <> place from encode <> [num limits.offset, num limits.limit]) <&> fmap refs+  where+    who = [tip, xpubId spec, num n0, num n1]+    refs rows =+      [ TxRef {block = position s, txid = h}+      | [m, sc] <- groups 2 rows,+        Right h <- [decode m],+        Just s <- [integer sc]+      ]++-- | A page of the unspent outputs of a cached key, newest first, each with+-- where its address is in the key ('paths').+cachedUnspents ::+  Connection ->+  FleetKeys ->+  ByteString ->+  XPubSpec ->+  Entry ->+  From ->+  Limits ->+  IO (Cached [XPubUnspent])+cachedUnspents conn keys tip spec e from limits =+  ask conn keys "outs" (who <> place from first <> [num limits.offset, num limits.limit]) <&> \case+    Hit rows -> maybe Miss Hit (mapM output (groups 3 rows))+    Miss -> Miss+    Moved -> Moved+  where+    (n0, n1) = e.sizes+    who = [tip, xpubId spec, num n0, num n1]+    -- the outputs of the transaction that a page starts at come first+    -- among those of its second+    first h = encode (OutPoint h 0)+    output [o, sc, kept] = do+      outpoint <- either (const Nothing) Just (decode o)+      s <- integer sc+      let (value, more) = C.break (== ' ') kept+          (size, rest) = C.break (== ' ') (B.drop 1 more)+          (a, script) = B.splitAt (number size) (B.drop 1 rest)+      address <- either (const Nothing) Just (decode a)+      path <- HashMap.lookup address e.paths+      return+        XPubUnspent+          { path,+            unspent = Unspent {block = position s, outpoint, value = number value, script, address = Just address}+          }+    output _ = Nothing++-- | The number of transactions of a cached key.+cachedCount :: Connection -> FleetKeys -> ByteString -> XPubSpec -> (Int, Int) -> IO (Cached Word32)+cachedCount conn keys tip spec (n0, n1) =+  ask conn keys "count" [tip, xpubId spec, num n0, num n1] <&> \case+    Hit [n] -> Hit (number n)+    Hit _ -> Miss+    Miss -> Miss+    Moved -> Moved++-- | Where a page starts, as the arguments of the script, given the bytes+-- that the members of the score of a transaction are to be no smaller+-- than.+place :: From -> (TxHash -> ByteString) -> [ByteString]+place from bytes = case from of+  Top -> ["top", "", ""]+  InMempool time h -> ["at", num (score (MemRef time)), bytes h]+  InChain ref -> ["score", num (score ref), ""]++-- | Ask the script something that it answers with rows.+ask :: Connection -> FleetKeys -> ByteString -> [ByteString] -> IO (Cached [ByteString])+ask conn keys what args =+  run conn (R.eval xpubScript [] (what : keys.prefix : "32" : args)) >>= \case+    "ok" : rows -> return (Hit rows)+    ["miss"] -> return Miss+    ["raced"] -> return Moved+    other -> fail ("Unexpected answer of the xpub script: " <> show (take 1 other))++-------------------------------------------------------------------------------+-- Writing+-------------------------------------------------------------------------------++-- | What became of an attempt to build an entry.+data Built+  = -- | the entry has the addresses of the key now, of which so many were+    -- added+    Built !Int+  | -- | the fleet tip moved, the cache is not at the tip, or another store+    -- wrote the entry meanwhile: nothing more was written+    Abandoned+  deriving (Eq, Show)++-- | The numbers of addresses that the cache has for a key in each of its+-- two chains, both zero if it does not have the key.+cacheHave :: Connection -> FleetKeys -> XPubSpec -> IO (Int, Int)+cacheHave conn keys spec =+  run conn (R.eval xpubScript [] ["have", keys.prefix, "32", xpubId spec]) >>= \case+    [n0, n1] -> return (fromInteger n0, fromInteger n1)+    other -> fail ("Unexpected answer of the xpub script: " <> show (other :: [Integer]))++-- | Delete the entry of a key.+cacheDrop :: Connection -> FleetKeys -> XPubSpec -> IO ()+cacheDrop conn keys spec = do+  _ <- run conn (R.eval xpubScript [] ["drop", keys.prefix, "32", xpubId spec]) :: IO [ByteString]+  return ()++-- | Build the entry of a key, or add to it the addresses that the key has+-- gained: given the tip that the fleet is at, the database as it was at+-- that block, the pool, and the time in milliseconds.+--+-- The addresses of the key are read as a request reads them, from the+-- chain with the pool on top. An entry that has some of them keeps what it+-- has and gets the others; one that has an address that the key no longer+-- has is deleted and built again. The chain's part of each new address is+-- read from the database, and sent a few addresses at a time, so that no+-- one write holds Redis for long: until the last is in, the entry has+-- fewer addresses than the key, and every read of it misses.+--+-- Each write is taken only while the fleet tip, the block of the cache+-- and the addresses of the entry are what this store found.+cacheBuild :: Connection -> FleetKeys -> ByteString -> DatabaseReader -> PoolReads -> Word64 -> XPubSpec -> IO Built+cacheBuild conn keys tip r pool now spec = do+  bals <- (pooledRead r pool).xPubBals spec+  let wanted m = [b | b@XPubBal {path = m' : _} <- bals, m' == m]+      (w0, w1) = (length (wanted 0), length (wanted 1))+  (h0, h1) <- cacheHave conn keys spec+  (n0, n1) <-+    if h0 > w0 || h1 > w1+      then cacheDrop conn keys spec >> return (0, 0)+      else return (h0, h1)+  let new = drop n0 (wanted 0) <> drop n1 (wanted 1)+  go (n0, n1) 0 =<< mapM chainOf new+  where+    chain = pooledRead r nothingPool+    chainOf b = do+      let a = b.balance.address+      bal <- Q.getBalance r a+      txs <- chain.getAddressTxs a def+      outs <- chain.getAddressUnspents a def+      return (b, bal, txs, outs)+    weight (_, _, txs, outs) = 1 + length txs + length outs+    -- as many addresses as hold about 'chunk' rows between them+    batch _ [] = ([], [])+    batch room (x : xs)+      | room <= 0 = ([], x : xs)+      | otherwise = let (now', later) = batch (room - weight x) xs in (x : now', later)+    go _ added [] = return (Built added)+    go (n0, n1) added todo = do+      let (now', later) = batch chunk todo+          args =+            [tip, xpubId spec, num n0, num n1, num now, num (length now')]+              <> concatMap address now'+          count m = length [() | (XPubBal {path = m' : _}, _, _, _) <- now', m' == m]+      run conn (R.eval xpubScript [] ("build" : keys.prefix : "32" : args)) >>= \case+        ["ok"] -> go (n0 + count 0, n1 + count 1) (added + length now') later+        [why] | why `elem` ["raced", "behind", "changed"] -> return Abandoned+        other -> fail ("Unexpected answer of the xpub script: " <> show (take 1 (other :: [ByteString])))+    address (b, bal, txs, outs) =+      [encode b.balance.address, path b.path, maybe "" encode (bal >>= real)]+        <> [num (length txs)]+        <> concat [[encode t.txid, num (score t.block)] | t <- txs]+        <> [num (length outs)]+        <> concat [[encode u.outpoint, num (score u.block), num u.value, u.script] | u <- outs]+    real b = if nullBalance b then Nothing else Just b+    path = \case+      [m, n] -> runPut (putWord8 (fromIntegral m) >> putWord32be n)+      _ -> ""++-- | About how many transactions and outputs one write of 'cacheBuild'+-- carries.+chunk :: Int+chunk = 5000++-- | What became of an attempt to bring the cache to a block.+data CacheMoved+  = -- | the cache is at the block now, with what the blocks did+    CacheMoved+  | -- | it was there already+    CacheStays+  | -- | the fleet tip is not that block any more: whoever moves it next+    -- sees to the cache+    CacheRaced+  | -- | the cache was emptied and starts again at the block+    CacheEmptied+  deriving (Eq, Show)++-- | What blocks did at an address: the transactions that they added to+-- it, the outputs that they paid to it, and its outputs that they spent.+data AddressChange = AddressChange+  { txs :: ![(TxHash, BlockRef)],+    paid :: ![(OutPoint, BlockRef, Word64, ByteString)],+    spent :: ![OutPoint]+  }+  deriving (Eq, Show)++instance Semigroup AddressChange where+  a <> b = AddressChange {txs = a.txs <> b.txs, paid = a.paid <> b.paid, spent = a.spent <> b.spent}++-- | What the transactions of some blocks, in their order, did at each+-- address that they touch. An output that one of them pays and a later one+-- spends is in both lists of its address: 'cacheAdvance' takes out what+-- was spent after it puts in what was paid.+blockChanges :: Network -> Ctx -> [TxData] -> HashMap Address AddressChange+blockChanges net ctx tds = HashMap.fromListWith (flip (<>)) (concatMap one tds)+  where+    one td =+      let h = txHash td.tx+          address = either (const Nothing) Just . scriptToAddressBS net ctx+          ins =+            [ (a, AddressChange {txs = [], paid = [], spent = [i.outpoint]})+            | (n, i) <- zip [0 ..] td.tx.inputs,+              i.outpoint /= nullOutPoint,+              Just prev <- [IntMap.lookup n td.prevs],+              Just a <- [address prev.script]+            ]+          outs =+            [ (a, AddressChange {txs = [], paid = [(OutPoint h n, td.block, o.value, o.script)], spent = []})+            | (n, o) <- zip [0 ..] td.tx.outputs,+              Just a <- [address o.script]+            ]+          touched = HashMap.keys (HashMap.fromList [(a, ()) | (a, _) <- ins <> outs])+       in [(a, AddressChange {txs = [(h, td.block)], paid = [], spent = []}) | a <- touched] <> ins <> outs++-- | Which of some addresses an entry of the cache has, in their order.+cacheTracked :: Connection -> FleetKeys -> [Address] -> IO [Bool]+cacheTracked conn keys as = concat <$> forM (pieces as) asked+  where+    asked some =+      map (== '1') . C.unpack+        <$> run conn (R.eval xpubScript [] ("tracked" : keys.prefix : "32" : map encode some))+    pieces [] = []+    pieces xs = let (now, later) = splitAt 4000 xs in now : pieces later++-- | Bring the cache from the block that it is taken to be at to the block+-- that the fleet tip has moved to: given what the blocks in between did at+-- the addresses that entries have, each with its balance in the chain at+-- the new block, or 'Nothing' to empty the cache instead, as a store does+-- that cannot say what they did.+--+-- The cache is also emptied when it is found at another block than the+-- one given: a store moved the tip and did not get to bring the cache+-- along, and what the blocks since did to it is not known here.+cacheAdvance ::+  Connection ->+  FleetKeys ->+  Maybe ByteString ->+  ByteString ->+  Maybe [(Address, Maybe Balance, AddressChange)] ->+  IO CacheMoved+cacheAdvance conn keys from to changes =+  run conn (R.eval xpubScript [] ("advance" : keys.prefix : "32" : args)) >>= \case+    ["moved"] -> return CacheMoved+    ["stays"] -> return CacheStays+    ["raced"] -> return CacheRaced+    ["emptied"] -> return CacheEmptied+    other -> fail ("Unexpected answer of the xpub script: " <> show (take 1 (other :: [ByteString])))+  where+    args =+      [fromMaybe "" from, to, maybe "1" (const "0") changes, num (maybe 0 length changes)]+        <> concatMap one (fromMaybe [] changes)+    one (a, bal, c) =+      [encode a, maybe "" encode (bal >>= real), num (length c.txs)]+        <> concat [[encode h, num (score ref)] | (h, ref) <- c.txs]+        <> [num (length c.paid)]+        <> concat [[encode p, num (score ref), num v, s] | (p, ref, v, s) <- c.paid]+        <> [num (length c.spent)]+        <> map encode c.spent+    real b = if nullBalance b then Nothing else Just b++-- | Delete the least recently used entries past so many. Says how many it+-- deleted.+cacheEvict :: Connection -> FleetKeys -> Int -> IO Int+cacheEvict conn keys most =+  fromInteger <$> run conn (R.eval xpubScript [] ["evict", keys.prefix, "32", num most])++-- | How large the cache is: the number of its entries, one for each+-- extended public key, and the number of addresses that they have between+-- them, which is what its memory in Redis goes with, together with the+-- transactions of those addresses.+cacheSize :: Connection -> FleetKeys -> IO (Int, Int)+cacheSize conn keys =+  run conn (R.eval xpubScript [] ["size", keys.prefix, "32"]) >>= \case+    [entries, addresses] -> return (fromInteger entries, fromInteger addresses)+    other -> fail ("Unexpected answer of the xpub script: " <> show (other :: [Integer]))++-- | The block that the cache is at, if it is where the fleet tip is: at+-- the same block, by the same move of the tip.+cacheTip :: Connection -> FleetKeys -> IO (Maybe ByteString)+cacheTip conn keys =+  run conn (R.eval xpubScript [] ["where", keys.prefix, "32"]) <&> \block ->+    if B.null block then Nothing else Just block++-------------------------------------------------------------------------------+-- Helpers+-------------------------------------------------------------------------------++num :: (Show a) => a -> ByteString+num = C.pack . show++number :: (Num a) => ByteString -> a+number = maybe 0 (fromInteger . fst) . C.readInteger++-- | A score as Redis prints it: a whole number, which every score of an+-- entry is, or, should Redis ever print a large one that way, a number in+-- the notation of floating point numbers. The first is read as a whole+-- number, since reading a floating point number from text takes several+-- microseconds, and a page has a score for each row.+integer :: ByteString -> Maybe Int64+integer bytes = case C.readInteger bytes of+  Just (n, rest) | B.null rest -> Just (fromInteger n)+  _ -> case reads (C.unpack bytes) :: [(Double, String)] of+    [(d, "")] -> Just (round d)+    _ -> Nothing++groups :: Int -> [a] -> [[a]]+groups n rows+  | null rows = []+  | otherwise = take n rows : groups n (drop n rows)++-- | 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)++-- | The script of the cache; see its header for what it takes and gives.+xpubScript :: ByteString+xpubScript = $(makeRelativeToProject "src/Haskoin/Store/Fleet/lua/xpub.lua" >>= embedFile)
+ src/Haskoin/Store/Fleet/lua/move.lua view
@@ -0,0 +1,74 @@+-- Move the tip to the newest block that every live store reported, if the+-- tip is still the one that the caller saw: moveScript in+-- Haskoin.Store.Fleet.Redis, and agreed and move in Haskoin.Store.Fleet.+--+-- KEYS: the tip, the leases, the recent blocks.+-- ARGV: the tip that the caller saw, or '' for none, the time in+-- milliseconds or '' for the clock of Redis, and the size of a block in+-- bytes.+--+-- Returns {'moved', block}, {'stays'} or {'raced'}.+local seen, clock, size = ARGV[1], ARGV[2], tonumber(ARGV[3])++local tip = redis.call('GET', KEYS[1]) or ''+if tip ~= seen then+  return {'raced'}+end++local now+if clock == '' then+  local t = redis.call('TIME')+  now = t[1] * 1000 + math.floor(t[2] / 1000)+else+  now = tonumber(clock)+end++-- the stores whose lease has not run out, in the order of their names+local live = {}+local leases = redis.call('HGETALL', KEYS[2])+for i = 1, #leases, 2 do+  if tonumber(leases[i + 1]) > now then+    live[#live + 1] = leases[i]+  end+end+if #live == 0 then+  return {'stays'}+end+table.sort(live)++local reports = {}+for i, store in ipairs(live) do+  reports[i] = redis.call('HGET', KEYS[3], store) or ''+end++-- the first block of one report that every other report has+local first = reports[1]+for i = 1, #first, size do+  local block = string.sub(first, i, i + size - 1)+  local everywhere = true+  for j = 2, #reports do+    local found = false+    for k = 1, #reports[j], size do+      if string.sub(reports[j], k, k + size - 1) == block then+        found = true+        break+      end+    end+    if not found then+      everywhere = false+      break+    end+  end+  if everywhere then+    if block == tip then+      return {'stays'}+    end+    redis.call('SET', KEYS[1], block)+    -- the moves of the tip are counted, next to it: see pool.lua+    redis.call('INCR', string.sub(KEYS[1], 1, -4) .. 'moves')+    -- and told to the stores, as pool.lua tells it+    redis.call('XADD', string.sub(KEYS[1], 1, -4) .. 'events', 'MAXLEN', '~', '10000', '*', 'k', 'b', 'h', block)+    return {'moved', block}+  end+end+return {'stays'}
+ src/Haskoin/Store/Fleet/lua/pool.lua view
@@ -0,0 +1,477 @@+-- The shared mempool of a fleet: poolScript in Haskoin.Store.Fleet.Redis,+-- and offer, withdraw and advance in Haskoin.Store.Fleet.Pool. The three are+-- one script because they share how a transaction is put in and taken out.+--+-- ARGV[1] is what to do, ARGV[2] the prefix of the keys, and ARGV[3] the+-- size of a transaction id in bytes. An output is the id of the transaction+-- that made it and four more bytes. Lists are their items one after the+-- other.+--+-- A transaction is given as its id, the time it entered a mempool, the+-- outputs that it spends, its body, '1' if it says itself that it can be+-- replaced by fee or '0', the number of its entries by address, and then+-- five arguments for each entry: its kind, the address, an output+-- or '', an amount or '', and a script or ''. The kinds are 't', the+-- transaction touches the address; 's', it spends that output of the+-- address, of that amount; and 'o', it pays that output to the address,+-- with that amount and script.+--+-- 'offer': the tip that the store saw or '', a string with '1' for each+-- output that the transaction spends and that the store finds unspent at+-- the tip and '0' for the others, the transactions that it replaces, and+-- the transaction. Returns {'taken'}, {'known'}, {'conflict', ...},+-- {'orphan', ...} or {'raced'}.+--+-- 'withdraw': the transactions. Returns those that left.+--+-- 'advance': the tip that the store saw or '', the block to move to, the+-- time in milliseconds or '' for the clock of Redis, the size of a block in+-- bytes, '1' to empty the pool instead of changing it or '0', the+-- transactions that are gone, those that are confirmed, the outputs that+-- are spent, the number of transactions that come back, those+-- transactions, and the blocks that the tip moves through, oldest first,+-- or ''. Returns {'moved'}, {'stays'} or {'raced'}. The pool is+-- emptied for a store that cannot say what the blocks between the two tips+-- did, and only when no other store is live: {'raced'} otherwise.+--+-- What the pool holds for each address, kept in step with its transactions:+-- its transactions and the outputs paid to it, both ordered newest first,+-- the outputs of it that are spent, and the sums of all that, which are the+-- summary of Haskoin.Store.Database.Types.MemSummary. It also holds every+-- transaction by the time it entered a mempool, which is the list of the+-- mempool, and by its id alone, to find those whose id starts with some+-- bytes.+--+-- A transaction can be replaced by fee if it says so itself or if it spends+-- from a transaction of the pool that can be. The second changes when a+-- parent is confirmed or comes back from an abandoned block, so the pool+-- keeps the answer apart from the body, and corrects it for what spends a+-- transaction whenever that one comes or goes. An output counts in+-- 'both' while the pool has both the entry that pays it and the entry that+-- spends it, so it is added by the second to come and taken off by the+-- first to go.+--+-- Every move of the tip is counted, in 'fleet:moves'. The cache of extended+-- public keys answers at one move of the tip and not at one block, since+-- the tip can leave a block and come back to it, and what the pool did to+-- the entries on the way is not undone by coming back.+--+-- The cache of extended public keys (xpub.lua) has, for each key that it+-- holds, the transactions of its addresses and the outputs paid to them,+-- those of the pool among them. So what the pool puts in or takes out at an+-- address goes in or out of the entry of every key that has the address,+-- here, in the same step.+local what, prefix, tsize = ARGV[1], ARGV[2], tonumber(ARGV[3])+local osize = tsize + 4+local key = {+  tip = prefix .. 'fleet:tip',+  moves = prefix .. 'fleet:moves',+  events = prefix .. 'fleet:events',+  lease = prefix .. 'fleet:lease',+  recent = prefix .. 'fleet:recent',+  spends = prefix .. 'pool:spends',+  spender = prefix .. 'pool:spender',+  body = prefix .. 'pool:body',+  kids = prefix .. 'pool:kids:',+  entries = prefix .. 'pool:entries',+  time = prefix .. 'pool:time',+  ids = prefix .. 'pool:ids',+  own = prefix .. 'pool:own',+  rbf = prefix .. 'pool:rbf',+  atx = prefix .. 'pool:atx:',+  aout = prefix .. 'pool:aout:',+  aval = prefix .. 'pool:aval:',+  aspent = prefix .. 'pool:aspent:',+  asum = prefix .. 'pool:asum:',+  of = prefix .. 'fx:of:',+  entry = prefix .. 'fx:e:'+}++local function split(s, n)+  local items = {}+  for i = 1, #s, n do+    items[#items + 1] = string.sub(s, i, i + n - 1)+  end+  return items+end++-- Tell every store of the fleet that something happened: 'b' and a block+-- that the tip moved to or through, 't' and a transaction that entered the+-- pool, 'd' and one that left it unconfirmed. The events go in a stream, in+-- the step that makes the change, so that every store serves the same+-- events in the same order, whatever its own node saw and when. The stream+-- keeps about the last ten thousand.+local function emit(kind, what)+  redis.call('XADD', key.events, 'MAXLEN', '~', '10000', '*', 'k', kind, 'h', what)+end++local function member(t)+  return redis.call('HEXISTS', key.spends, t) == 1+end++-- the transactions of the pool among these, and all that spend them+local function descendants(from)+  local seen, order, todo = {}, {}, {}+  for _, t in ipairs(from) do+    if member(t) then+      todo[#todo + 1] = t+    end+  end+  while #todo > 0 do+    local t = table.remove(todo)+    if not seen[t] then+      seen[t] = true+      order[#order + 1] = t+      for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do+        todo[#todo + 1] = kid+      end+    end+  end+  return order, seen+end++-- a transaction of the arguments, from a position, and the position after+-- it+local function read(at)+  local tx = {id = ARGV[at], time = tonumber(ARGV[at + 1]), inputs = ARGV[at + 2], body = ARGV[at + 3]}+  tx.own = ARGV[at + 4] == '1'+  local entries = {}+  local n = tonumber(ARGV[at + 5])+  at = at + 6+  for i = 1, n do+    entries[i] = {ARGV[at], ARGV[at + 1], ARGV[at + 2], ARGV[at + 3], ARGV[at + 4]}+    at = at + 5+  end+  tx.entries = entries+  return tx, at+end++local function add(a, field, by)+  redis.call('HINCRBY', key.asum .. a, field, string.format('%.0f', by))+end++-- put the entries of a transaction in, with sign 1, or take them out, with+-- sign -1+local function account(t, time, entries, sign)+  local touched = {}+  for _, e in ipairs(entries) do+    local kind, a, o, v = e[1], e[2], e[3], tonumber(e[4])+    touched[a] = true+    -- the cached keys that have the address+    local watching = {}+    if kind ~= 's' then+      watching = redis.call('SMEMBERS', key.of .. a)+    end+    if kind == 't' then+      if sign > 0 then+        redis.call('ZADD', key.atx .. a, -time, t)+      else+        redis.call('ZREM', key.atx .. a, t)+      end+      for _, id in ipairs(watching) do+        if sign > 0 then+          redis.call('ZADD', key.entry .. id .. ':txs', -time, t)+        else+          redis.call('ZREM', key.entry .. id .. ':txs', t)+        end+      end+      add(a, 'txs', sign)+    elseif kind == 's' then+      if sign > 0 then+        redis.call('HSET', key.aspent .. a, o, e[4])+      else+        redis.call('HDEL', key.aspent .. a, o)+      end+      add(a, 'spent', sign * v)+      add(a, 'spentCount', sign)+      if redis.call('HEXISTS', key.aval .. a, o) == 1 then+        add(a, 'both', sign * v)+        add(a, 'bothCount', sign)+      end+    else+      if sign > 0 then+        redis.call('ZADD', key.aout .. a, -time, o)+        redis.call('HSET', key.aval .. a, o, e[4] .. ' ' .. e[5])+      else+        redis.call('ZREM', key.aout .. a, o)+        redis.call('HDEL', key.aval .. a, o)+      end+      for _, id in ipairs(watching) do+        if sign > 0 then+          redis.call('ZADD', key.entry .. id .. ':outs', -time, o)+          redis.call('HSET', key.entry .. id .. ':oval', o, e[4] .. ' ' .. #a .. ' ' .. a .. e[5])+        else+          redis.call('ZREM', key.entry .. id .. ':outs', o)+          redis.call('HDEL', key.entry .. id .. ':oval', o)+        end+      end+      add(a, 'paid', sign * v)+      add(a, 'paidCount', sign)+      if redis.call('HEXISTS', key.aspent .. a, o) == 1 then+        add(a, 'both', sign * v)+        add(a, 'bothCount', sign)+      end+    end+  end+  -- an address that no transaction touches has no summary+  if sign < 0 then+    for a, _ in pairs(touched) do+      if tonumber(redis.call('HGET', key.asum .. a, 'txs')) == 0 then+        redis.call('DEL', key.asum .. a)+      end+    end+  end+end++-- work out again whether a transaction of the pool can be replaced by+-- fee, and if that changed, the same for what spends it+local function replaceable(t)+  local inputs = redis.call('HGET', key.spends, t)+  if not inputs then+    return+  end+  local can = redis.call('SISMEMBER', key.own, t) == 1+  if not can then+    for _, o in ipairs(split(inputs, osize)) do+      if redis.call('SISMEMBER', key.rbf, string.sub(o, 1, tsize)) == 1 then+        can = true+        break+      end+    end+  end+  if can ~= (redis.call('SISMEMBER', key.rbf, t) == 1) then+    if can then+      redis.call('SADD', key.rbf, t)+    else+      redis.call('SREM', key.rbf, t)+    end+    for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do+      replaceable(kid)+    end+  end+end++-- take one transaction out, leaving what spends it: quietly if a block+-- confirmed it, which is no event of the mempool+local function remove(t, quiet)+  local inputs = redis.call('HGET', key.spends, t)+  if not inputs then+    return+  end+  if not quiet then+    emit('d', t)+  end+  local kept = cmsgpack.unpack(redis.call('HGET', key.entries, t))+  account(t, kept[1], kept[2], -1)+  redis.call('HDEL', key.entries, t)+  redis.call('ZREM', key.time, t)+  redis.call('ZREM', key.ids, t)+  for _, o in ipairs(split(inputs, osize)) do+    redis.call('HDEL', key.spender, o)+    redis.call('SREM', key.kids .. string.sub(o, 1, tsize), t)+  end+  redis.call('HDEL', key.spends, t)+  redis.call('HDEL', key.body, t)+  redis.call('SREM', key.own, t)+  if redis.call('SREM', key.rbf, t) == 1 then+    for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do+      replaceable(kid)+    end+  end+end++local function insert(tx)+  local t = tx.id+  redis.call('HSET', key.spends, t, tx.inputs)+  redis.call('HSET', key.body, t, tx.body)+  for _, o in ipairs(split(tx.inputs, osize)) do+    redis.call('HSET', key.spender, o, t)+    redis.call('SADD', key.kids .. string.sub(o, 1, tsize), t)+  end+  redis.call('HSET', key.entries, t, cmsgpack.pack({tx.time, tx.entries}))+  redis.call('ZADD', key.time, tx.time, t)+  redis.call('ZADD', key.ids, 0, t)+  account(t, tx.time, tx.entries, 1)+  if tx.own then+    redis.call('SADD', key.own, t)+  end+  replaceable(t)+  emit('t', t)+end++local function withdraw(from)+  local order = descendants(from)+  for _, t in ipairs(order) do+    remove(t)+  end+  return order+end++if what == 'withdraw' then+  return withdraw(split(ARGV[4], tsize))+end++local tip = redis.call('GET', key.tip) or ''+if tip ~= ARGV[4] then+  return {'raced'}+end++if what == 'offer' then+  local flags = ARGV[5]+  local tx = read(7)+  local t, inputs = tx.id, split(tx.inputs, osize)+  if member(t) then+    return {'known'}+  end+  -- what it replaces leaves first, but only if it is taken+  local leaving, left = descendants(split(ARGV[6], tsize))+  local rivals, counted = {}, {}+  for _, o in ipairs(inputs) do+    local s = redis.call('HGET', key.spender, o)+    if s and not left[s] and not counted[s] then+      counted[s] = true+      rivals[#rivals + 1] = s+    end+  end+  if #rivals > 0 then+    table.insert(rivals, 1, 'conflict')+    return rivals+  end+  local orphans = {'orphan'}+  for i, o in ipairs(inputs) do+    local maker = string.sub(o, 1, tsize)+    local made = member(maker) and not left[maker]+    if not made and string.sub(flags, i, i) ~= '1' then+      orphans[#orphans + 1] = o+    end+  end+  if #orphans > 1 then+    return orphans+  end+  for _, gone in ipairs(leaving) do+    remove(gone)+  end+  insert(tx)+  return {'taken'}+end++-- advance: the block must be the one that the live stores agree on+local target, clock, bsize = ARGV[5], ARGV[6], tonumber(ARGV[7])+if target == tip then+  return {'stays'}+end+local now+if clock == '' then+  local time = redis.call('TIME')+  now = time[1] * 1000 + math.floor(time[2] / 1000)+else+  now = tonumber(clock)+end+local live = {}+local leases = redis.call('HGETALL', key.lease)+for i = 1, #leases, 2 do+  if tonumber(leases[i + 1]) > now then+    live[#live + 1] = leases[i]+  end+end+if #live == 0 then+  return {'raced'}+end+table.sort(live)+local reports = {}+for i, store in ipairs(live) do+  reports[i] = redis.call('HGET', key.recent, store) or ''+end+local agreed = ''+for _, block in ipairs(split(reports[1], bsize)) do+  local everywhere = true+  for j = 2, #reports do+    local found = false+    for _, other in ipairs(split(reports[j], bsize)) do+      if other == block then+        found = true+        break+      end+    end+    if not found then+      everywhere = false+      break+    end+  end+  if everywhere then+    agreed = block+    break+  end+end+if agreed ~= target then+  return {'raced'}+end++-- a store that is alone and does not know the way from the old tip starts+-- the pool again+if ARGV[8] == '1' then+  if #live ~= 1 then+    return {'raced'}+  end+  for _, k in ipairs(redis.call('KEYS', prefix .. 'pool:*')) do+    redis.call('DEL', k)+  end+  redis.call('SET', key.tip, target)+  redis.call('INCR', key.moves)+  emit('b', target)+  return {'moved'}+end++-- the abandoned blocks: what can come back does, and what spends the rest+-- leaves+local at = 13+for _ = 1, tonumber(ARGV[12]) do+  local tx+  tx, at = read(at)+  if not member(tx.id) then+    -- it was confirmed: what spends the same in the pool gives way+    local rivals = {}+    for _, o in ipairs(split(tx.inputs, osize)) do+      local s = redis.call('HGET', key.spender, o)+      if s then+        rivals[#rivals + 1] = s+      end+    end+    withdraw(rivals)+    insert(tx)+  end+end+local orphaned = {}+for _, t in ipairs(split(ARGV[9], tsize)) do+  for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do+    orphaned[#orphaned + 1] = kid+  end+end+withdraw(orphaned)++-- the new blocks: what they confirm leaves alone, and what spends what they+-- spend leaves with all that spends it+for _, t in ipairs(split(ARGV[10], tsize)) do+  remove(t, true)+end+local clashing = {}+for _, o in ipairs(split(ARGV[11], osize)) do+  local s = redis.call('HGET', key.spender, o)+  if s then+    clashing[#clashing + 1] = s+  end+end+withdraw(clashing)++redis.call('SET', key.tip, target)+redis.call('INCR', key.moves)+-- the blocks that the tip moved through, oldest first, if the store that+-- moves it gave them, and the block that it is at now otherwise+local path = split(ARGV[at] or '', bsize)+if #path == 0 then+  path = {target}+end+for _, block in ipairs(path) do+  emit('b', block)+end+return {'moved'}
+ src/Haskoin/Store/Fleet/lua/read.lua view
@@ -0,0 +1,172 @@+-- What a store reads from the shared mempool of its fleet: readScript in+-- Haskoin.Store.Fleet.Redis. pool.lua writes what this reads.+--+-- ARGV[1] is what to read, ARGV[2] the prefix of the keys, ARGV[3] the size+-- of a transaction id in bytes, and ARGV[4] the fleet tip that the store+-- answers from, or ''. Every answer is {'raced'} if the tip is another one:+-- the pool goes with the tip, and laid over the chain of another block it+-- would give a transaction twice or not at all. Otherwise it is {'ok', ...}.+--+-- 'sums': addresses. For each, its seven sums separated by spaces, in the+-- order of the summary, or '' if the pool has nothing for it.+--+-- 'txs': an address, the time and id of the transaction to start at or two+-- '', and how many. Transactions of the address from there, newest first:+-- an id and a time for each.+--+-- 'outs': an address, the time and output to start at or two '', and how+-- many. Outputs paid to the address from there, newest first: for each the+-- output, its time, its amount and script separated by a space, and '1' if+-- the pool spends it or '0'.+--+-- 'spent': an address and outputs. A string with '1' for each output of the+-- address that the pool spends and '0' for the others.+--+-- 'bodies': transactions, of the pool or not. For each: its body or '',+-- '1' if it can be replaced by fee or '0', the number of its outputs that+-- the pool spends, and for each of those the output, the transaction that+-- spends it and the number of its input that does.+--+-- 'spenders': outputs. For each, the transaction that spends it and the+-- number of its input that does, or two ''.+--+-- 'page': an offset and how many, 0 for all. Transactions of the pool from+-- the newest, an id and a time for each.+--+-- 'ids': some bytes. The transactions whose id starts with them.+local what, prefix, tsize = ARGV[1], ARGV[2], tonumber(ARGV[3])+local osize = tsize + 4+if (redis.call('GET', prefix .. 'fleet:tip') or '') ~= ARGV[4] then+  return {'raced'}+end+local answer = {'ok'}++local function split(s, n)+  local items = {}+  for i = 1, #s, n do+    items[#items + 1] = string.sub(s, i, i + n - 1)+  end+  return items+end++-- whether one string of bytes sorts before another, as Redis sorts the+-- members of a sorted set; the comparison of Lua goes by the locale+local function before(a, b)+  local n = math.min(#a, #b)+  for i = 1, n do+    local x, y = string.byte(a, i), string.byte(b, i)+    if x ~= y then+      return x < y+    end+  end+  return #a < #b+end++-- the position in a sorted set, ordered newest first, of the member with a+-- time and a name, or where it would be+local function position(zkey, time, member)+  if time == '' then+    return 0+  end+  local score = -tonumber(time)+  local at = redis.call('ZCOUNT', zkey, '-inf', '(' .. string.format('%.0f', score))+  for _, m in ipairs(redis.call('ZRANGEBYSCORE', zkey, score, score)) do+    if before(m, member) then+      at = at + 1+    end+  end+  return at+end++if what == 'sums' then+  for i = 5, #ARGV do+    local s = redis.call('HMGET', prefix .. 'pool:asum:' .. ARGV[i],+      'txs', 'paid', 'paidCount', 'spent', 'spentCount', 'both', 'bothCount')+    if s[1] then+      for j = 1, 7 do+        s[j] = s[j] or '0'+      end+      answer[#answer + 1] = table.concat(s, ' ')+    else+      answer[#answer + 1] = ''+    end+  end+elseif what == 'txs' then+  local zkey = prefix .. 'pool:atx:' .. ARGV[5]+  local at = position(zkey, ARGV[6], ARGV[7])+  local rows = redis.call('ZRANGE', zkey, at, at + tonumber(ARGV[8]) - 1, 'WITHSCORES')+  for i = 1, #rows, 2 do+    answer[#answer + 1] = rows[i]+    answer[#answer + 1] = string.format('%.0f', -tonumber(rows[i + 1]))+  end+elseif what == 'outs' then+  local a = ARGV[5]+  local zkey = prefix .. 'pool:aout:' .. a+  local at = position(zkey, ARGV[6], ARGV[7])+  local rows = redis.call('ZRANGE', zkey, at, at + tonumber(ARGV[8]) - 1, 'WITHSCORES')+  for i = 1, #rows, 2 do+    answer[#answer + 1] = rows[i]+    answer[#answer + 1] = string.format('%.0f', -tonumber(rows[i + 1]))+    answer[#answer + 1] = redis.call('HGET', prefix .. 'pool:aval:' .. a, rows[i])+    answer[#answer + 1] = tostring(redis.call('HEXISTS', prefix .. 'pool:aspent:' .. a, rows[i]))+  end+elseif what == 'spent' then+  local flags = {}+  for i, o in ipairs(split(ARGV[6], osize)) do+    flags[i] = tostring(redis.call('HEXISTS', prefix .. 'pool:aspent:' .. ARGV[5], o))+  end+  answer[2] = table.concat(flags)+elseif what == 'bodies' then+  for _, t in ipairs(split(ARGV[5], tsize)) do+    answer[#answer + 1] = redis.call('HGET', prefix .. 'pool:body', t) or ''+    answer[#answer + 1] = tostring(redis.call('SISMEMBER', prefix .. 'pool:rbf', t))+    -- what spends its outputs: the inputs of the transactions of the pool+    -- that spend from it+    local spent = {}+    for _, kid in ipairs(redis.call('SMEMBERS', prefix .. 'pool:kids:' .. t)) do+      for i, input in ipairs(split(redis.call('HGET', prefix .. 'pool:spends', kid), osize)) do+        if string.sub(input, 1, tsize) == t then+          spent[#spent + 1] = input+          spent[#spent + 1] = kid+          spent[#spent + 1] = tostring(i - 1)+        end+      end+    end+    answer[#answer + 1] = tostring(#spent / 3)+    for _, item in ipairs(spent) do+      answer[#answer + 1] = item+    end+  end+elseif what == 'spenders' then+  for _, o in ipairs(split(ARGV[5], osize)) do+    local s = redis.call('HGET', prefix .. 'pool:spender', o)+    local index = ''+    if s then+      for i, input in ipairs(split(redis.call('HGET', prefix .. 'pool:spends', s), osize)) do+        if input == o then+          index = tostring(i - 1)+        end+      end+    end+    answer[#answer + 1] = s or ''+    answer[#answer + 1] = index+  end+elseif what == 'page' then+  local offset, many = tonumber(ARGV[5]), tonumber(ARGV[6])+  local last = -1+  if many > 0 then+    last = offset + many - 1+  end+  local rows = redis.call('ZREVRANGE', prefix .. 'pool:time', offset, last, 'WITHSCORES')+  for i = 1, #rows, 2 do+    answer[#answer + 1] = rows[i]+    answer[#answer + 1] = string.format('%.0f', tonumber(rows[i + 1]))+  end+elseif what == 'ids' then+  local found = redis.call('ZRANGEBYLEX', prefix .. 'pool:ids',+    '[' .. ARGV[5], '(' .. ARGV[5] .. string.rep(string.char(255), tsize))+  for _, t in ipairs(found) do+    answer[#answer + 1] = t+  end+end+return answer
+ src/Haskoin/Store/Fleet/lua/report.lua view
@@ -0,0 +1,54 @@+-- Note the recent blocks of a store under a lease, if the store may report:+-- reportScript in Haskoin.Store.Fleet.Redis, and mayReport and report in+-- Haskoin.Store.Fleet.+--+-- KEYS: the tip, the leases, the recent blocks.+-- ARGV: the store, the length of the lease in milliseconds, the time in+-- milliseconds or '' for the clock of Redis, the size of a block in bytes,+-- the recent blocks of the store, newest first, and the blocks that it has+-- views of, each as blocks one after the other.+--+-- Returns 1 if the report was taken and 0 if the store may not report.+local store, lease, clock, size = ARGV[1], tonumber(ARGV[2]), ARGV[3], tonumber(ARGV[4])+local recent, views = ARGV[5], ARGV[6]++local now+if clock == '' then+  local t = redis.call('TIME')+  now = t[1] * 1000 + math.floor(t[2] / 1000)+else+  now = tonumber(clock)+end++-- whether the store has a view of the tip+local tip = redis.call('GET', KEYS[1])+local has = not tip+if tip then+  for i = 1, #views, size do+    if string.sub(views, i, i + size - 1) == tip then+      has = true+      break+    end+  end+end++-- whether another store is live; leases that ran out are forgotten+local alone = true+local leases = redis.call('HGETALL', KEYS[2])+for i = 1, #leases, 2 do+  if tonumber(leases[i + 1]) > now then+    if leases[i] ~= store then+      alone = false+    end+  else+    redis.call('HDEL', KEYS[2], leases[i])+    redis.call('HDEL', KEYS[3], leases[i])+  end+end++if not (has or alone) then+  return 0+end+redis.call('HSET', KEYS[2], store, now + lease)+redis.call('HSET', KEYS[3], store, recent)+return 1
+ src/Haskoin/Store/Fleet/lua/xpub.lua view
@@ -0,0 +1,401 @@+-- The cache of extended public keys of a fleet: xpubScript in+-- Haskoin.Store.Fleet.XPub, which says what an entry is and why. pool.lua+-- keeps the part of each entry that comes from the shared mempool.+--+-- ARGV[1] is what to do, ARGV[2] the prefix of the keys, and ARGV[3] the+-- size of a transaction id in bytes. An output is the id of the transaction+-- that made it and four more bytes. A position in a list is a score, as+-- Haskoin.Store.Fleet.XPub.score gives it: lists are ordered by score and,+-- within one score, by the bytes of the member. What an entry keeps for an+-- output is its amount, a space, the length of its address, a space, the+-- address, and the script.+--+-- The reads take the fleet tip that the store answers from, the id of a+-- key, and the number of its addresses in each of its two chains as the+-- caller has them. They answer {'raced'} if the fleet tip is another block,+-- {'miss'} if the cache is not where the tip is, if it does not have the key,+-- or if it has it with other addresses, and {'ok', ...} otherwise.+--+-- 'bals': the tip, the id, and the time in milliseconds, or '' not to note+-- that the key was used. The numbers of addresses are not given, and not+-- checked. For each address of the entry: the address, its path followed+-- by its balance in the chain if it has one, and its seven sums in the+-- pool separated by spaces or ''. A path is five bytes: the chain of the+-- key that the address is in, and its number there.+--+-- 'txs', 'outs': the tip, the id, the two numbers, where to start, an+-- offset and how many, 0 for all. Where to start is 'top' and two '';+-- 'score' with a score and ''; or 'at' with a score and the bytes that the+-- members of that score are to be no smaller than. 'txs' gives a+-- transaction and a score for each; 'outs' an output, a score and what is+-- kept for it, leaving out the outputs that the pool spends.+--+-- 'count': the tip, the id and the two numbers. The number of transactions.+--+-- 'have': the id. The two numbers of the entry, whatever block the cache is+-- at: {n0, n1}.+--+-- 'build': the tip, the id, the two numbers that the entry is to have+-- before, the time in milliseconds, the number of addresses to add, and for+-- each: the address, its path, its balance in the chain or '', the number+-- of its transactions in the chain, a transaction and a score for each, the+-- number of its unspent outputs in the chain, and an output, a score, an+-- amount and a script for each. What the pool has for the address is added+-- from the pool. Returns {'ok'}; {'raced'} if the fleet tip is another+-- block; {'behind'} if the cache is at another block; {'changed'} if the+-- entry does not have those two numbers of addresses.+--+-- 'drop': the id. Deletes the entry.+--+-- 'tracked': addresses. A string with '1' for each address that an entry+-- has and '0' for the others.+--+-- 'where': nothing. The block that the cache is at, if it is at the fleet+-- tip as it stands, and '' otherwise.+--+-- 'advance': the block that the cache is taken to be at or '', the block to+-- bring it to, '1' to empty it or '0', the number of addresses that the+-- blocks in between changed, and for each: the address, its balance in+-- the chain at the new block or '', the number of transactions that the+-- blocks added to it, a transaction and a score for each, the number of+-- outputs that they paid to it, an output, a score, an amount and a script+-- for each, the number of its outputs that they spent, and those outputs.+-- Returns {'moved'}; {'stays'} if the cache is at the new block already;+-- {'raced'} if the fleet tip is not the new block; and {'emptied'} if it+-- had entries and was asked to empty them, or was not where the tip was+-- one move ago, in which case what happened since cannot be told.+--+-- 'size': nothing. The number of entries, and the number of addresses that+-- they have between them: {entries, addresses}.+--+-- 'evict': how many entries to keep. Deletes the least recently used ones+-- past that many, and says how many it deleted.+local what, prefix, tsize = ARGV[1], ARGV[2], tonumber(ARGV[3])+local key = {+  fleet = prefix .. 'fleet:tip',+  tip = prefix .. 'fx:tip',+  lru = prefix .. 'fx:lru',+  addrs = prefix .. 'fx:addrs',+  of = prefix .. 'fx:of:',+  entry = prefix .. 'fx:e:',+  spender = prefix .. 'pool:spender',+  atx = prefix .. 'pool:atx:',+  aout = prefix .. 'pool:aout:',+  aval = prefix .. 'pool:aval:',+  asum = prefix .. 'pool:asum:'+}++-- whether one string of bytes sorts before another, as Redis sorts the+-- members of a sorted set; the comparison of Lua goes by the locale+local function before(a, b)+  local n = math.min(#a, #b)+  for i = 1, n do+    local x, y = string.byte(a, i), string.byte(b, i)+    if x ~= y then+      return x < y+    end+  end+  return #a < #b+end++local function kept(a, amount, script)+  return amount .. ' ' .. #a .. ' ' .. a .. script+end++local function drop(id)+  local e = key.entry .. id .. ':'+  local had = redis.call('HKEYS', e .. 'addrs')+  for _, a in ipairs(had) do+    redis.call('SREM', key.of .. a, id)+  end+  if #had > 0 then+    redis.call('DECRBY', key.addrs, #had)+  end+  redis.call('DEL', e .. 'addrs', e .. 'meta', e .. 'txs', e .. 'outs', e .. 'oval')+  redis.call('ZREM', key.lru, id)+end++local function empty()+  for _, id in ipairs(redis.call('ZRANGE', key.lru, 0, -1)) do+    drop(id)+  end+  -- whatever a script that failed half way left behind+  for _, k in ipairs(redis.call('KEYS', prefix .. 'fx:*')) do+    redis.call('DEL', k)+  end+end++local function numbers(e)+  local n = redis.call('HMGET', e .. 'meta', 'n0', 'n1')+  return tonumber(n[1] or '0'), tonumber(n[2] or '0')+end++if what == 'have' then+  local n0, n1 = numbers(key.entry .. ARGV[4] .. ':')+  return {n0, n1}+elseif what == 'drop' then+  drop(ARGV[4])+  return {'ok'}+elseif what == 'tracked' then+  local flags = {}+  for i = 4, #ARGV do+    flags[#flags + 1] = tostring(redis.call('EXISTS', key.of .. ARGV[i]))+  end+  return table.concat(flags)+elseif what == 'size' then+  return {redis.call('ZCARD', key.lru), tonumber(redis.call('GET', key.addrs) or '0')}+elseif what == 'evict' then+  local over = redis.call('ZCARD', key.lru) - tonumber(ARGV[4])+  if over <= 0 then+    return 0+  end+  for _, id in ipairs(redis.call('ZRANGE', key.lru, 0, over - 1)) do+    drop(id)+  end+  return over+end++local fleet = redis.call('GET', key.fleet) or ''++-- Where the cache is, is a move of the fleet tip and the block that it went+-- to. A block alone would not do: the tip can leave a block and come back+-- to it, as when a block is reverted and mined again, and a cache that was+-- not brought along would find itself at the tip, with entries that the+-- pool has taken the transactions of that block out of on the way.+local moves = tonumber(redis.call('GET', prefix .. 'fleet:moves') or '0')+local function place(n, block)+  return n .. '@' .. block+end+local here = place(moves, fleet)++if what == 'where' then+  if (redis.call('GET', key.tip) or '') == here then+    return fleet+  end+  return ''+end++if what == 'advance' then+  local from, to = ARGV[4], ARGV[5]+  if fleet ~= to then+    return {'raced'}+  end+  local at = redis.call('GET', key.tip) or ''+  if at == here then+    return {'stays'}+  end+  -- it can be brought along only from the move before this one+  local held = redis.call('ZCARD', key.lru) > 0+  if held and (ARGV[6] == '1' or at ~= place(moves - 1, from)) then+    empty()+    redis.call('SET', key.tip, here)+    return {'emptied'}+  end+  local changed = tonumber(ARGV[7])+  local i = 8+  for _ = 1, changed do+    local a, bal = ARGV[i], ARGV[i + 1]+    local ids = redis.call('SMEMBERS', key.of .. a)+    local txs, outs, spent = {}, {}, {}+    local n = tonumber(ARGV[i + 2])+    i = i + 3+    for _ = 1, n do+      txs[#txs + 1] = {ARGV[i], ARGV[i + 1]}+      i = i + 2+    end+    n = tonumber(ARGV[i])+    i = i + 1+    for _ = 1, n do+      outs[#outs + 1] = {ARGV[i], ARGV[i + 1], kept(a, ARGV[i + 2], ARGV[i + 3])}+      i = i + 4+    end+    n = tonumber(ARGV[i])+    i = i + 1+    for _ = 1, n do+      spent[#spent + 1] = ARGV[i]+      i = i + 1+    end+    for _, id in ipairs(ids) do+      local e = key.entry .. id .. ':'+      local row = redis.call('HGET', e .. 'addrs', a)+      if row then+        redis.call('HSET', e .. 'addrs', a, string.sub(row, 1, 5) .. bal)+      end+      for _, t in ipairs(txs) do+        redis.call('ZADD', e .. 'txs', t[2], t[1])+      end+      for _, o in ipairs(outs) do+        redis.call('ZADD', e .. 'outs', o[2], o[1])+        redis.call('HSET', e .. 'oval', o[1], o[3])+      end+      for _, o in ipairs(spent) do+        redis.call('ZREM', e .. 'outs', o)+        redis.call('HDEL', e .. 'oval', o)+      end+    end+  end+  redis.call('SET', key.tip, here)+  return {'moved'}+end++-- everything else is for a store that answers from the fleet tip+local tip, id = ARGV[4], ARGV[5]+local e = key.entry .. id .. ':'+if fleet ~= tip then+  return {'raced'}+end++if what == 'build' then+  local at = redis.call('GET', key.tip)+  if not at then+    -- nothing is cached yet: the cache starts here+    redis.call('SET', key.tip, here)+  elseif at ~= here then+    return {'behind'}+  end+  local n0, n1 = numbers(e)+  if n0 ~= tonumber(ARGV[6]) or n1 ~= tonumber(ARGV[7]) then+    return {'changed'}+  end+  redis.call('ZADD', key.lru, ARGV[8], id)+  local i = 10+  for _ = 1, tonumber(ARGV[9]) do+    local a, path, bal = ARGV[i], ARGV[i + 1], ARGV[i + 2]+    redis.call('HSET', e .. 'addrs', a, path .. bal)+    redis.call('SADD', key.of .. a, id)+    if string.byte(path, 1) == 0 then+      n0 = n0 + 1+    else+      n1 = n1 + 1+    end+    local n = tonumber(ARGV[i + 3])+    i = i + 4+    for _ = 1, n do+      redis.call('ZADD', e .. 'txs', ARGV[i + 1], ARGV[i])+      i = i + 2+    end+    n = tonumber(ARGV[i])+    i = i + 1+    for _ = 1, n do+      redis.call('ZADD', e .. 'outs', ARGV[i + 1], ARGV[i])+      redis.call('HSET', e .. 'oval', ARGV[i], kept(a, ARGV[i + 2], ARGV[i + 3]))+      i = i + 4+    end+    -- what the pool has for the address, which pool.lua keeps up from now+    local rows = redis.call('ZRANGE', key.atx .. a, 0, -1, 'WITHSCORES')+    for j = 1, #rows, 2 do+      redis.call('ZADD', e .. 'txs', rows[j + 1], rows[j])+    end+    rows = redis.call('ZRANGE', key.aout .. a, 0, -1, 'WITHSCORES')+    for j = 1, #rows, 2 do+      local o = rows[j]+      local value = redis.call('HGET', key.aval .. a, o)+      local space = string.find(value, ' ', 1, true)+      redis.call('ZADD', e .. 'outs', rows[j + 1], o)+      redis.call('HSET', e .. 'oval', o, kept(a, string.sub(value, 1, space - 1), string.sub(value, space + 1)))+    end+  end+  redis.call('HSET', e .. 'meta', 'n0', n0, 'n1', n1)+  redis.call('INCRBY', key.addrs, tonumber(ARGV[9]))+  return {'ok'}+end++if (redis.call('GET', key.tip) or '') ~= here then+  return {'miss'}+end+local answer = {'ok'}++if what == 'bals' then+  local addrs = redis.call('HGETALL', e .. 'addrs')+  if #addrs == 0 then+    return {'miss'}+  end+  if ARGV[6] ~= '' then+    redis.call('ZADD', key.lru, ARGV[6], id)+  end+  -- the pool has a summary for an address when one of its transactions+  -- touches it, and those transactions are in the entry, with scores+  -- below zero: most keys have none, and are spared a read for each+  -- address+  local touched = redis.call('ZCOUNT', e .. 'txs', '-inf', '(0') > 0+  for i = 1, #addrs, 2 do+    local a = addrs[i]+    answer[#answer + 1] = a+    answer[#answer + 1] = addrs[i + 1]+    local sums = ''+    if touched then+      local s = redis.call('HMGET', key.asum .. a,+        'txs', 'paid', 'paidCount', 'spent', 'spentCount', 'both', 'bothCount')+      if s[1] then+        for j = 1, 7 do+          s[j] = s[j] or '0'+        end+        sums = table.concat(s, ' ')+      end+    end+    answer[#answer + 1] = sums+  end+  return answer+end++local n0, n1 = numbers(e)+if n0 + n1 == 0 or n0 ~= tonumber(ARGV[6]) or n1 ~= tonumber(ARGV[7]) then+  return {'miss'}+end++if what == 'count' then+  return {'ok', tostring(redis.call('ZCARD', e .. 'txs'))}+end++-- 'txs' and 'outs': the place in the list where the page starts+local zkey = e .. what+local mode, score, bytes = ARGV[8], ARGV[9], ARGV[10]+local offset, many = tonumber(ARGV[11]), tonumber(ARGV[12])+local first = 0+if mode ~= 'top' then+  first = redis.call('ZCOUNT', zkey, '-inf', '(' .. score)+  if mode == 'at' then+    for _, m in ipairs(redis.call('ZRANGEBYSCORE', zkey, score, score)) do+      if before(m, bytes) then+        first = first + 1+      end+    end+  end+end++if what == 'txs' then+  local last = -1+  if many > 0 then+    last = first + offset + many - 1+  end+  local rows = redis.call('ZRANGE', zkey, first + offset, last, 'WITHSCORES')+  for _, row in ipairs(rows) do+    answer[#answer + 1] = row+  end+  return answer+end++-- 'outs': those that the pool spends are left out, before the offset+local found, skipped, size = 0, 0, 64+while many == 0 or found < many do+  local rows = redis.call('ZRANGE', zkey, first, first + size - 1, 'WITHSCORES')+  if #rows == 0 then+    break+  end+  for j = 1, #rows, 2 do+    local o = rows[j]+    if redis.call('HEXISTS', key.spender, o) == 0 then+      if skipped < offset then+        skipped = skipped + 1+      elseif many == 0 or found < many then+        found = found + 1+        answer[#answer + 1] = o+        answer[#answer + 1] = rows[j + 1]+        answer[#answer + 1] = redis.call('HGET', e .. 'oval', o)+      end+    end+  end+  first = first + size+  size = math.min(4096, size * 4)+end+return answer
src/Haskoin/Store/Logic.hs view
@@ -1,13 +1,5 @@-{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-}  -- |@@ -18,12 +10,19 @@ -- Stability   : experimental -- Portability : POSIX ----- Rules to import and revert blocks and transactions.+-- Rules to import and revert blocks and transactions in the layout of+-- version 20 of the database, in which the mempool is written into the+-- indices of the chain. The store no longer imports with them: it runs on+-- the layered layout, with the rules of "Haskoin.Store.Logic.Layered".+-- These are kept as the reference: the tests give the same random histories+-- to both and compare every result and every answer, and that comparison is+-- the evidence that the new layout does what the old one did. -- -- These functions keep every index consistent: transactions, unspent outputs, -- spenders, address transactions, address unspent outputs, balances and the--- mempool. They run in 'ImportM', which buffers all changes and commits them in--- one atomic batch, so a failure leaves the database untouched.+-- mempool. Each takes the 'Writer' of a batch as its first argument and runs+-- in 'IO'; the writer buffers all changes and 'runWriter' commits them in one+-- atomic batch, so a failure leaves the database untouched. -- -- When a block is imported, transactions already in the mempool are confirmed, -- new ones are imported, and mempool transactions that conflict with the block@@ -51,9 +50,13 @@ -- transactions that conflict with it are deleted together with everything that -- spends them. Between mempool transactions, replace-by-fee decides on networks -- that support it; elsewhere the first one seen stays.+--+-- These rules write unconfirmed transactions into the same indices as the+-- chain. "Haskoin.Store.Logic.Layered" has the rules for a layout that keeps+-- them apart, with the same functions; the store will move to it when that+-- layout replaces this one. module Haskoin.Store.Logic   ( ImportException (..),-    ImportM,     initBest,     revertBlock,     importBlock,@@ -71,11 +74,11 @@     when,     zipWithM_,   )-import qualified Data.ByteString as B+import Data.ByteString qualified as B import Data.Either (rights)-import qualified Data.HashMap.Strict as M-import qualified Data.HashSet as H-import qualified Data.IntMap.Strict as I+import Data.HashMap.Strict qualified as M+import Data.HashSet qualified as H+import Data.IntMap.Strict qualified as I import Data.List (nub) import Data.Maybe   ( catMaybes,@@ -113,21 +116,35 @@     txHashToHex,   ) import Haskoin.Node (LogLevel (..), logAt)-import Haskoin.Store.Common+import Haskoin.Store.Common (nub', sortTxs) import Haskoin.Store.Data-import Haskoin.Store.Database.Writer (WriterT, writerLogger)+import Haskoin.Store.Database.Writer hiding (deleteSpender, insertSpender) import UnliftIO (Exception, throwIO) --- | Block and transaction import. Failures are thrown as 'ImportException'.--- Writes are buffered and committed together when 'runWriter' finishes, so a--- failed batch commits nothing.-type ImportM = WriterT IO- -- | Log a message with the logger of the writer.-logDebug, logError :: Text -> ImportM ()-logDebug msg = writerLogger >>= \l -> logAt LevelDebug l msg-logError msg = writerLogger >>= \l -> logAt LevelError l msg+logDebug, logError :: Writer -> Text -> IO ()+logDebug wr = logAt LevelDebug (writerLogger wr)+logError wr = logAt LevelError (writerLogger wr) +-- | Network of the database being written.+getNetwork :: Writer -> IO Network+getNetwork wr = return (writerNet wr)++-- | Secp256k1 context of the database being written.+getCtx :: Writer -> IO Ctx+getCtx wr = return (writerCtx wr)++-- | Data of a transaction that is in the database and not deleted.+getActiveTxData :: Writer -> TxHash -> IO (Maybe TxData)+getActiveTxData wr th =+  getTxData wr th >>= \case+    Just td | not td.deleted -> return (Just td)+    _ -> return Nothing++-- | Balance of an address, or a zero balance if it was never used.+getDefaultBalance :: Writer -> Address -> IO Balance+getDefaultBalance wr a = fromMaybe (zeroBalance a) <$> getBalance wr a+ -- | Reasons why an import can fail. data ImportException   = -- | the block does not build on the best block@@ -169,14 +186,14 @@   show TxSpent = "Transaction is spent"  -- | Import the genesis block if the database has no best block yet.-initBest :: ImportM ()-initBest = do-  logDebug "Initializing best block"-  net <- getNetwork-  m <- getBestBlock+initBest :: Writer -> IO ()+initBest wr = do+  logDebug wr "Initializing best block"+  net <- getNetwork wr+  m <- getBestBlock wr   when (isNothing m) . void $ do-    logDebug "Importing Genesis block"-    importBlock (genesisBlock net) (genesisNode net)+    logDebug wr "Importing Genesis block"+    importBlock wr (genesisBlock net) (genesisNode net)  -- | Import a transaction into the mempool with the time it arrived. Returns -- 'False' if it is already known.@@ -185,84 +202,84 @@ -- descendants if the network has replace-by-fee; otherwise the transaction is -- rejected with 'DoubleSpend'. Throws an 'ImportException' if the transaction is -- invalid, for example if it spends unknown outputs.-newMempoolTx :: Tx -> UnixTime -> ImportM Bool-newMempoolTx tx w =-  getActiveTxData (txHash tx) >>= \case+newMempoolTx :: Writer -> Tx -> UnixTime -> IO Bool+newMempoolTx wr tx w =+  getActiveTxData wr (txHash tx) >>= \case     Just _ ->       return False     Nothing -> do       -- Check everything before writing, so that a rejected transaction       -- leaves the mempool as it was.-      replaced <- getConflicts True True tx-      checkNewTx (map txHash replaced) tx-      mapM_ (deleteSingleTx . txHash) replaced-      rbf <- isRBF (MemRef w) tx-      _ <- importTx (MemRef w) w rbf tx+      replaced <- getConflicts wr True True tx+      checkNewTx wr (map txHash replaced) tx+      mapM_ (deleteSingleTx wr . txHash) replaced+      rbf <- isRBF wr (MemRef w) tx+      _ <- importTx wr (MemRef w) w rbf tx       return True  -- | Data of the best block. Throws 'BestBlockNotFound' if there is none.-bestBlockData :: ImportM BlockData-bestBlockData = do+bestBlockData :: Writer -> IO BlockData+bestBlockData wr = do   h <--    getBestBlock >>= \case+    getBestBlock wr >>= \case       Nothing -> do-        logError "Best block unknown"+        logError wr "Best block unknown"         throwIO BestBlockNotFound       Just h -> return h-  getBlock h >>= \case+  getBlock wr h >>= \case     Nothing -> do-      logError "Best block not found"+      logError wr "Best block not found"       throwIO BestBlockNotFound     Just b -> return b  -- | Revert the best block: make its parent the best block, mark it as not in the -- main chain, move its transactions back to the mempool, and delete its -- coinbase. Throws 'BlockNotBest' if the block is not the best block.-revertBlock :: BlockHash -> ImportM ()-revertBlock bh = do+revertBlock :: Writer -> BlockHash -> IO ()+revertBlock wr bh = do   bd <--    bestBlockData >>= \b ->+    bestBlockData wr >>= \b ->       if headerHash b.header == bh         then return b         else do-          logError $+          logError wr $             "Cannot revert non-head block: " <> blockHashToHex bh           throwIO BlockNotBest-  logDebug $+  logDebug wr $     "Obtained block data for " <> blockHashToHex bh-  tds <- mapM getImportTxData bd.txs-  logDebug $+  tds <- mapM (getImportTxData wr) bd.txs+  logDebug wr $     "Obtained import tx data for block " <> blockHashToHex bh-  setBest bd.header.prev-  logDebug $+  setBest wr bd.header.prev+  logDebug wr $     "Set parent as best block "       <> blockHashToHex bd.header.prev-  insertBlock bd {main = False}-  logDebug $+  insertBlock wr bd {main = False}+  logDebug wr $     "Updated as not in main chain: " <> blockHashToHex bh-  forM_ (drop 1 tds) unConfirmTx-  logDebug $+  forM_ (drop 1 tds) (unConfirmTx wr)+  logDebug wr $     "Unconfirmed " <> cs (show (length tds)) <> " transactions"-  deleteConfirmedTx (txHash (tds !! 0).tx)-  logDebug $+  deleteConfirmedTx wr (txHash (tds !! 0).tx)+  logDebug wr $     "Deleted coinbase: " <> txHashToHex (txHash (tds !! 0).tx)  -- | Check that a block builds on the best block, or that it is the genesis block -- of an empty database.-checkNewBlock :: Block -> BlockNode -> ImportM ()-checkNewBlock b n =-  getBestBlock >>= \case+checkNewBlock :: Writer -> Block -> BlockNode -> IO ()+checkNewBlock wr b n =+  getBestBlock wr >>= \case     Nothing       | isGenesis n -> return ()       | otherwise -> do-          logError $+          logError wr $             "Cannot import non-genesis block: "               <> blockHashToHex (headerHash b.header)           throwIO BestBlockNotFound     Just h       | b.header.prev == h -> return ()       | otherwise -> do-          logError $+          logError wr $             "Block does not build on head: "               <> blockHashToHex (headerHash b.header)           throwIO PrevBlockNotBest@@ -270,57 +287,66 @@ -- | Import the transactions of a block, in dependency order. Mempool transactions -- that conflict with the block are deleted first. Transactions already in the -- mempool are confirmed; others are imported.-importOrConfirm :: BlockNode -> [Tx] -> ImportM [TxData]-importOrConfirm bn txns = do-  logDebug "Freeing outputs..."-  mapM_ (freeOutputs True False . snd) (reverse txs)-  logDebug "Outputs freed"+importOrConfirm :: Writer -> BlockNode -> [Tx] -> IO [TxData]+importOrConfirm wr bn txns = do+  logDebug wr "Freeing outputs..."+  mapM_ (freeOutputs wr True False . snd) (reverse txs)+  logDebug wr "Outputs freed"   mapM (uncurry action) txs   where     txs = sortTxs txns     br i = BlockRef {height = bn.height, position = i}     bn_time = fromIntegral $ bn.header.timestamp     action i tx =-      testPresent tx >>= \case+      testPresent wr tx >>= \case         False -> import_it i tx         True -> confirm_it i tx     confirm_it i tx =-      getActiveTxData (txHash tx) >>= \case+      getActiveTxData wr (txHash tx) >>= \case         Just t -> do-          logDebug $+          logDebug wr $             "Confirming tx: "               <> txHashToHex (txHash tx)-          confirmTx t (br i)+          -- with the time of the block, as the layered rules give it: see+          -- 'Haskoin.Store.Logic.Layered.importChainTx'. Version 20 kept+          -- the time at which the store first saw the transaction; these+          -- rules are what the tests hold the layered ones to, and have+          -- to agree with them on it.+          td <- confirmTx wr t (br i)+          let timed = (td :: TxData) {timestamp = bn_time}+          insertTx wr timed+          return timed         Nothing -> do-          logError $+          logError wr $             "Cannot find tx to confirm: "               <> txHashToHex (txHash tx)           throwIO TxNotFound     import_it i tx = do-      logDebug $+      logDebug wr $         "Importing tx: " <> txHashToHex (txHash tx)-      importTx (br i) bn_time False tx+      importTx wr (br i) bn_time False tx  -- | Import a block that builds on the best block, and make it the best block. -- Returns its data and the data of its transactions.-importBlock :: Block -> BlockNode -> ImportM (BlockData, [TxData])-importBlock b n = do-  logDebug $+importBlock :: Writer -> Block -> BlockNode -> IO (BlockData, [TxData])+importBlock wr b n = do+  logDebug wr $     "Checking new block: "       <> blockHashToHex (headerHash n.header)-  checkNewBlock b n-  logDebug "Passed check"-  net <- getNetwork+  checkNewBlock wr b n+  logDebug wr "Passed check"+  net <- getNetwork wr   let subsidy = computeSubsidy net n.height-  bs <- getBlocksAtHeight n.height-  logDebug $+  bs <- getBlocksAtHeight wr n.height+  logDebug wr $     "Inserting block entries for: "       <> blockHashToHex (headerHash n.header)   setBlocksAtHeight+    wr     (nub (headerHash n.header : bs))     n.height-  setBest (headerHash n.header)-  tds <- importOrConfirm n b.txs+  setBest wr (headerHash n.header)+  tds <- importOrConfirm wr n b.txs   let bd =         BlockData           { height = n.height,@@ -334,8 +360,8 @@             fee = sum $ map txDataFee tds,             outputs = ts_out_val           }-  insertBlock bd-  logDebug $+  insertBlock wr bd+  logDebug wr $     "Finished importing block: "       <> blockHashToHex (headerHash n.header)   return (bd, tds)@@ -351,39 +377,39 @@  -- | Check a transaction that will replace these ones. Outputs that they spend -- count as unspent, and outputs that they create as missing.-checkNewTx :: [TxHash] -> Tx -> ImportM ()-checkNewTx replaced tx = do+checkNewTx :: Writer -> [TxHash] -> Tx -> IO ()+checkNewTx wr replaced tx = do   when (unique_inputs < length tx.inputs) $ do-    logError $+    logError wr $       "Transaction spends same output twice: "         <> txHashToHex (txHash tx)     throwIO DuplicatePrevOutput   prevs <- mapM prev_output (prevOuts tx)   when (any isNothing prevs) $ do-    logError $+    logError wr $       "Orphan: " <> txHashToHex (txHash tx)     throwIO Orphan   when (isCoinbaseTx tx) $ do-    logError $+    logError wr $       "Coinbase cannot be imported into mempool: "         <> txHashToHex (txHash tx)     throwIO UnexpectedCoinbase   when (outputs > sum (map (.value) (catMaybes prevs))) $ do-    logError $+    logError wr $       "Insufficient funds for tx: " <> txHashToHex (txHash tx)     throwIO InsufficientFunds   where     prev_output op       | op.hash `elem` replaced = return Nothing       | otherwise = do-          parent <- getActiveTxData op.hash+          parent <- getActiveTxData wr op.hash           return (parent >>= getTxOut op.index . (.tx))     outputs = sum (map (.value) tx.outputs)     unique_inputs = length (nub' (map (.outpoint) tx.inputs))  -- | Unspent outputs spent by a transaction, or 'Nothing' for those that are missing.-getUnspentOutputs :: (StoreReadBase m) => Tx -> m [Maybe Unspent]-getUnspentOutputs tx = mapM getUnspent (prevOuts tx)+getUnspentOutputs :: Writer -> Tx -> IO [Maybe Unspent]+getUnspentOutputs wr tx = mapM (getUnspent wr) (prevOuts tx)  -- | Data of a new transaction, from its replace-by-fee flag, position, time and -- the outputs it spends.@@ -404,47 +430,50 @@  -- | Import a transaction at a position in the chain or the mempool. Throws 'Orphan' if it spends a missing output. importTx ::+  Writer ->   BlockRef ->   -- | unix time   Word64 ->   -- | RBF   Bool ->   Tx ->-  ImportM TxData-importTx br tt rbf tx = do-  mus <- getUnspentOutputs tx+  IO TxData+importTx wr br tt rbf tx = do+  mus <- getUnspentOutputs wr tx   us <- forM mus $ \case     Nothing -> do-      logError $+      logError wr $         "Attempted to import a tx missing UTXO: "           <> txHashToHex (txHash tx)       throwIO Orphan     Just u -> return u   let td = prepareTxData rbf br tt tx us-  commitAddTx td+  commitAddTx wr td   return td  -- | Move a confirmed transaction back to the mempool.-unConfirmTx :: TxData -> ImportM TxData-unConfirmTx t = confTx t Nothing+unConfirmTx :: Writer -> TxData -> IO TxData+unConfirmTx wr t = confTx wr t Nothing  -- | Confirm a mempool transaction at a position in a block.-confirmTx :: TxData -> BlockRef -> ImportM TxData-confirmTx t br = confTx t (Just br)+confirmTx :: Writer -> TxData -> BlockRef -> IO TxData+confirmTx wr t br = confTx wr t (Just br)  -- | Move a transaction to a new position in the indices of all its addresses.-replaceAddressTx :: TxData -> BlockRef -> ImportM ()-replaceAddressTx t new = do-  net <- getNetwork-  ctx <- getCtx+replaceAddressTx :: Writer -> TxData -> BlockRef -> IO ()+replaceAddressTx wr t new = do+  net <- getNetwork wr+  ctx <- getCtx wr   forM_ (txDataAddresses net ctx t) $ \a -> do     deleteAddrTx+      wr       a       TxRef         { block = t.block,           txid = txHash t.tx         }     insertAddrTx+      wr       a       TxRef         { block = new,@@ -454,14 +483,15 @@ -- | Move an unspent output to a new position in the chain, updating the indices -- and balance of its address. adjustAddressOutput ::+  Writer ->   OutPoint ->   TxOut ->   BlockRef ->   BlockRef ->-  ImportM ()-adjustAddressOutput op o old new = do+  IO ()+adjustAddressOutput wr op o old new = do   let pk = o.script-  getUnspent op >>= \case+  getUnspent wr op >>= \case     Nothing -> return ()     Just u -> do       unless (u.block == old) $@@ -470,11 +500,12 @@       replace_unspent pk   where     replace_unspent pk = do-      net <- getNetwork-      ctx <- getCtx+      net <- getNetwork wr+      ctx <- getCtx wr       let ma = eitherToMaybe (scriptToAddressBS net ctx pk)-      deleteUnspent op+      deleteUnspent wr op       insertUnspent+        wr         Unspent           { block = new,             outpoint = op,@@ -485,6 +516,7 @@       forM_ ma $ replace_addr_unspent pk     replace_addr_unspent pk a = do       deleteAddrUnspent+        wr         a         Unspent           { block = old,@@ -494,6 +526,7 @@             address = Just a           }       insertAddrUnspent+        wr         a         Unspent           { block = new,@@ -502,22 +535,22 @@             script = pk,             address = Just a           }-      decreaseBalance (confirmed old) a o.value-      increaseBalance (confirmed new) a o.value+      decreaseBalance wr (confirmed old) a o.value+      increaseBalance wr (confirmed new) a o.value  -- | Confirm a transaction at a position in a block, or move it back to the mempool -- if no position is given.-confTx :: TxData -> Maybe BlockRef -> ImportM TxData-confTx t mbr = do-  replaceAddressTx t new+confTx :: Writer -> TxData -> Maybe BlockRef -> IO TxData+confTx wr t mbr = do+  replaceAddressTx wr t new   forM_ (zip [0 ..] t.tx.outputs) $ \(n, o) -> do     let op = OutPoint (txHash t.tx) n-    adjustAddressOutput op o old new-  rbf <- isRBF new t.tx+    adjustAddressOutput wr op o old new+  rbf <- isRBF wr new t.tx   let td = (t :: TxData) {block = new, rbf = rbf}-  insertTx td-  updateMempool td-  updateSpendersRBF td+  insertTx wr td+  updateMempool wr td+  updateSpendersRBF wr td   return td   where     new = fromMaybe (MemRef t.timestamp) mbr@@ -525,111 +558,114 @@  -- | Spenders inherit BIP125 signalling from unconfirmed parents only, so their -- flag can change when a parent is confirmed or unconfirmed.-updateSpendersRBF :: TxData -> ImportM ()-updateSpendersRBF td =+updateSpendersRBF :: Writer -> TxData -> IO ()+updateSpendersRBF wr td =   forM_ (nub' (map (.txid) (I.elems td.spenders))) $ \h ->-    getActiveTxData h >>= mapM_ update+    getActiveTxData wr h >>= mapM_ update   where     update s = do-      rbf <- isRBF s.block s.tx+      rbf <- isRBF wr s.block s.tx       when (rbf /= s.rbf) $ do         let s' = (s :: TxData) {rbf = rbf}-        insertTx s'-        updateSpendersRBF s'+        insertTx wr s'+        updateSpendersRBF wr s'  -- | Delete the transactions that spend the same outputs as this one, with their -- descendants. The first flag refuses to delete confirmed transactions, and the -- second refuses any deletion on networks without replace-by-fee.-freeOutputs :: Bool -> Bool -> Tx -> ImportM ()-freeOutputs memonly rbfcheck tx =-  getConflicts memonly rbfcheck tx >>= mapM_ (deleteSingleTx . txHash)+freeOutputs :: Writer -> Bool -> Bool -> Tx -> IO ()+freeOutputs wr memonly rbfcheck tx =+  getConflicts wr memonly rbfcheck tx >>= mapM_ (deleteSingleTx wr . txHash)  -- | Transactions that spend the same outputs as this one, with their -- descendants, in an order in which they can be deleted. getConflicts ::+  Writer ->   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete if network has RBF   Bool ->   Tx ->-  ImportM [Tx]-getConflicts memonly rbfcheck tx = do-  logDebug $+  IO [Tx]+getConflicts wr memonly rbfcheck tx = do+  logDebug wr $     "Getting conflicts for tx " <> txHashToHex (txHash tx) <> "..."   let prevs = prevOuts tx-  unspents <- mapM getUnspent prevs+  unspents <- mapM (getUnspent wr) prevs   let spents = [p | (p, Nothing) <- zip prevs unspents]-  spndrs <- catMaybes <$> mapM getSpender spents+  spndrs <- catMaybes <$> mapM (getSpender wr) spents   let txids = H.fromList $ filter (/= txHash tx) $ map (.txid) spndrs-  chains <- mapM (getChain memonly rbfcheck) (H.toList txids)+  chains <- mapM (getChain wr memonly rbfcheck) (H.toList txids)   return . reverse . map snd . sortTxs . M.elems $     M.fromList [(txHash t, t) | t <- concat chains]  -- | Delete a transaction and its descendants, confirmed or not.-deleteConfirmedTx :: TxHash -> ImportM ()-deleteConfirmedTx = deleteTx False False+deleteConfirmedTx :: Writer -> TxHash -> IO ()+deleteConfirmedTx wr = deleteTx wr False False  -- | Delete an unconfirmed transaction and its descendants. If the flag is set and -- the network has no replace-by-fee, throw 'DoubleSpend' instead. Does nothing if -- the transaction is not found.-deleteUnconfirmedTx :: Bool -> TxHash -> ImportM ()-deleteUnconfirmedTx rbfcheck th =-  getActiveTxData th >>= \case-    Just _ -> deleteTx True rbfcheck th+deleteUnconfirmedTx :: Writer -> Bool -> TxHash -> IO ()+deleteUnconfirmedTx wr rbfcheck th =+  getActiveTxData wr th >>= \case+    Just _ -> deleteTx wr True rbfcheck th     Nothing ->-      logDebug $+      logDebug wr $         "Not found or already deleted: " <> txHashToHex th  -- | Delete a transaction and its descendants. deleteTx ::+  Writer ->   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete if network has RBF   Bool ->   TxHash ->-  ImportM ()-deleteTx memonly rbfcheck th = do-  chain <- getChain memonly rbfcheck th-  logDebug $+  IO ()+deleteTx wr memonly rbfcheck th = do+  chain <- getChain wr memonly rbfcheck th+  logDebug wr $     "Deleting "       <> cs (show (length chain))       <> " txs from chain leading to "       <> txHashToHex th-  mapM_ (\t -> let h = txHash t in deleteSingleTx h >> return h) chain+  mapM_ (\t -> let h = txHash t in deleteSingleTx wr h >> return h) chain  -- | A transaction and all its descendants, in an order in which they can be -- deleted: spenders first. getChain ::+  Writer ->   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete if network has RBF   Bool ->   TxHash ->-  ImportM [Tx]-getChain memonly rbfcheck th' = do-  logDebug $+  IO [Tx]+getChain wr memonly rbfcheck th' = do+  logDebug wr $     "Getting chain for tx " <> txHashToHex th'   sort_clean <$> go M.empty (H.singleton th')   where     sort_clean = reverse . map snd . sortTxs     get_tx th =-      getActiveTxData th >>= \case+      getActiveTxData wr th >>= \case         Nothing -> do-          logDebug $+          logDebug wr $             "Transaction not found: " <> txHashToHex th           return Nothing         Just td           | memonly && confirmed td.block -> do-              logError $+              logError wr $                 "Transaction already confirmed: "                   <> txHashToHex th               throwIO TxConfirmed           | rbfcheck ->-              getNetwork >>= \net ->+              getNetwork wr >>= \net ->                 if net.replaceByFee                   then return $ Just td                   else do-                    logError $+                    logError wr $                       "Double-spending transaction: "                         <> txHashToHex th                     throwIO DoubleSpend@@ -643,7 +679,7 @@           txm' = txmn <> txm       if H.null pdg'         then do-          logDebug $+          logDebug wr $             "Chain for tx "               <> txHashToHex th'               <> " contains "@@ -653,43 +689,43 @@         else go txm' pdg'  -- | Delete a transaction that nothing spends. Throws 'TxSpent' otherwise.-deleteSingleTx :: TxHash -> ImportM ()-deleteSingleTx th =-  getActiveTxData th >>= \case+deleteSingleTx :: Writer -> TxHash -> IO ()+deleteSingleTx wr th =+  getActiveTxData wr th >>= \case     Nothing -> do-      logError $+      logError wr $         "Already deleted: " <> txHashToHex th       throwIO TxNotFound     Just td ->       if I.null td.spenders         then do-          logDebug $+          logDebug wr $             "Deleting tx: " <> txHashToHex th-          commitDelTx td+          commitDelTx wr td         else do-          logError $+          logError wr $             "Tried to delete spent tx: "               <> txHashToHex th           throwIO TxSpent  -- | Remove a transaction from every index and mark it as deleted.-commitDelTx :: TxData -> ImportM ()-commitDelTx = commitModTx False+commitDelTx :: Writer -> TxData -> IO ()+commitDelTx wr = commitModTx wr False  -- | Add a transaction to every index.-commitAddTx :: TxData -> ImportM ()-commitAddTx = commitModTx True+commitAddTx :: Writer -> TxData -> IO ()+commitAddTx wr = commitModTx wr True  -- | Add a transaction to every index, or remove it if the flag is 'False'.-commitModTx :: Bool -> TxData -> ImportM ()-commitModTx add tx_data = do-  net <- getNetwork-  ctx <- getCtx+commitModTx :: Writer -> Bool -> TxData -> IO ()+commitModTx wr add tx_data = do+  net <- getNetwork wr+  ctx <- getCtx wr   mapM_ mod_addr_tx (txDataAddresses net ctx td)   mod_outputs   mod_unspent-  insertTx td-  updateMempool td+  insertTx wr td+  updateMempool wr td   where     tx = td.tx     br = td.block@@ -697,50 +733,50 @@     tx_ref = TxRef br (txHash tx)     mod_addr_tx a       | add = do-          insertAddrTx a tx_ref-          modAddressCount add a+          insertAddrTx wr a tx_ref+          modAddressCount wr add a       | otherwise = do-          deleteAddrTx a tx_ref-          modAddressCount add a+          deleteAddrTx wr a tx_ref+          modAddressCount wr add a     mod_unspent-      | add = spendOutputs tx-      | otherwise = unspendOutputs tx+      | add = spendOutputs wr tx+      | otherwise = unspendOutputs wr tx     mod_outputs-      | add = addOutputs br tx-      | otherwise = delOutputs br tx+      | add = addOutputs wr br tx+      | otherwise = delOutputs wr br tx  -- | Add a transaction to the mempool if it is unconfirmed, or remove it otherwise.-updateMempool :: TxData -> ImportM ()-updateMempool td@TxData {deleted = True} =-  deleteFromMempool (txHash td.tx)-updateMempool td@TxData {block = MemRef t} =-  addToMempool (txHash td.tx) t-updateMempool td@TxData {block = BlockRef {}} =-  deleteFromMempool (txHash td.tx)+updateMempool :: Writer -> TxData -> IO ()+updateMempool wr td@TxData {deleted = True} =+  deleteFromMempool wr (txHash td.tx)+updateMempool wr td@TxData {block = MemRef t} =+  addToMempool wr (txHash td.tx) t+updateMempool wr td@TxData {block = BlockRef {}} =+  deleteFromMempool wr (txHash td.tx)  -- | Mark the outputs spent by a transaction as spent.-spendOutputs :: Tx -> ImportM ()-spendOutputs tx =-  zipWithM_ (spendOutput (txHash tx)) [0 ..] (prevOuts tx)+spendOutputs :: Writer -> Tx -> IO ()+spendOutputs wr tx =+  zipWithM_ (spendOutput wr (txHash tx)) [0 ..] (prevOuts tx)  -- | Add the outputs of a transaction as unspent.-addOutputs :: BlockRef -> Tx -> ImportM ()-addOutputs br tx =-  zipWithM_ (addOutput br . OutPoint (txHash tx)) [0 ..] tx.outputs+addOutputs :: Writer -> BlockRef -> Tx -> IO ()+addOutputs wr br tx =+  zipWithM_ (addOutput wr br . OutPoint (txHash tx)) [0 ..] tx.outputs  -- | Whether an unconfirmed transaction can be replaced by fee: it signals BIP125 -- in one of its inputs, or it spends an unconfirmed transaction that can be -- replaced. Always 'False' for confirmed transactions and on networks without -- replace-by-fee. isRBF ::-  (StoreReadBase m) =>+  Writer ->   BlockRef ->   Tx ->-  m Bool-isRBF br tx+  IO Bool+isRBF wr br tx   | confirmed br = return False   | otherwise =-      getNetwork >>= \net ->+      getNetwork wr >>= \net ->         if net.replaceByFee           then go           else return False@@ -752,7 +788,7 @@       let hs = nub' $ map (.outpoint.hash) tx.inputs           ck [] = return False           ck (h : hs') =-            getActiveTxData h >>= \case+            getActiveTxData wr h >>= \case               Nothing -> return False               Just t                 | confirmed t.block -> ck hs'@@ -761,29 +797,30 @@        in ck hs  -- | Add an unspent output.-addOutput :: BlockRef -> OutPoint -> TxOut -> ImportM ()-addOutput = modOutput True+addOutput :: Writer -> BlockRef -> OutPoint -> TxOut -> IO ()+addOutput wr = modOutput wr True  -- | Delete an output.-delOutput :: BlockRef -> OutPoint -> TxOut -> ImportM ()-delOutput = modOutput False+delOutput :: Writer -> BlockRef -> OutPoint -> TxOut -> IO ()+delOutput wr = modOutput wr False  -- | Add an unspent output, or delete it if the flag is 'False', updating the indices and balance of its address. modOutput ::+  Writer ->   Bool ->   BlockRef ->   OutPoint ->   TxOut ->-  ImportM ()-modOutput add br op o = do-  net <- getNetwork-  ctx <- getCtx+  IO ()+modOutput wr add br op o = do+  net <- getNetwork wr+  ctx <- getCtx wr   let ma = eitherToMaybe (scriptToAddressBS net ctx o.script)   mod_unspent ma   forM_ ma $ \a -> do     mod_addr_unspent a (u ma)-    modBalance (confirmed br) add a o.value-    modifyReceived a v+    modBalance wr (confirmed br) add a o.value+    modifyReceived wr a v   where     v       | add = (+ o.value)@@ -797,25 +834,25 @@           address = ma         }     mod_unspent ma-      | add = insertUnspent (u ma)-      | otherwise = deleteUnspent op+      | add = insertUnspent wr (u ma)+      | otherwise = deleteUnspent wr op     mod_addr_unspent-      | add = insertAddrUnspent-      | otherwise = deleteAddrUnspent+      | add = insertAddrUnspent wr+      | otherwise = deleteAddrUnspent wr  -- | Delete the outputs of a transaction.-delOutputs :: BlockRef -> Tx -> ImportM ()-delOutputs br tx =+delOutputs :: Writer -> BlockRef -> Tx -> IO ()+delOutputs wr br tx =   forM_ (zip [0 ..] tx.outputs) $ \(i, o) -> do     let op = OutPoint (txHash tx) i-    delOutput br op o+    delOutput wr br op o  -- | Data of a transaction. Throws 'TxNotFound' if it is missing or deleted.-getImportTxData :: TxHash -> ImportM TxData-getImportTxData th =-  getActiveTxData th >>= \case+getImportTxData :: Writer -> TxHash -> IO TxData+getImportTxData wr th =+  getActiveTxData wr th >>= \case     Nothing -> do-      logDebug $ "Tx not found: " <> txHashToHex th+      logDebug wr $ "Tx not found: " <> txHashToHex th       throwIO TxNotFound     Just d -> return d @@ -826,50 +863,50 @@   return $ tx.outputs !! fromIntegral i  -- | Record which input spends an output.-insertSpender :: OutPoint -> Spender -> ImportM ()-insertSpender op s = do-  td <- getImportTxData op.hash+insertSpender :: Writer -> OutPoint -> Spender -> IO ()+insertSpender wr op s = do+  td <- getImportTxData wr op.hash   let p = td.spenders       p' = I.insert (fromIntegral op.index) s p       td' = (td :: TxData) {spenders = p'}-  insertTx td'+  insertTx wr td'  -- | Forget which input spends an output.-deleteSpender :: OutPoint -> ImportM ()-deleteSpender op = do-  td <- getImportTxData op.hash+deleteSpender :: Writer -> OutPoint -> IO ()+deleteSpender wr op = do+  td <- getImportTxData wr op.hash   let p = td.spenders       p' = I.delete (fromIntegral op.index) p       td' = (td :: TxData) {spenders = p'}-  insertTx td'+  insertTx wr td'  -- | Spend an unspent output with an input of a transaction.-spendOutput :: TxHash -> Word32 -> OutPoint -> ImportM ()-spendOutput th ix op = do+spendOutput :: Writer -> TxHash -> Word32 -> OutPoint -> IO ()+spendOutput wr th ix op = do   u <--    getUnspent op >>= \case+    getUnspent wr op >>= \case       Just u -> return u       Nothing -> error $ "Could not find UTXO to spend: " <> show op-  deleteUnspent op-  insertSpender op (Spender th ix)+  deleteUnspent wr op+  insertSpender wr op (Spender th ix)   let pk = u.script-  net <- getNetwork-  ctx <- getCtx+  net <- getNetwork wr+  ctx <- getCtx wr   forM_ (scriptToAddressBS net ctx pk) $ \a -> do-    decreaseBalance (confirmed u.block) a u.value-    deleteAddrUnspent a u+    decreaseBalance wr (confirmed u.block) a u.value+    deleteAddrUnspent wr a u  -- | Make the outputs spent by a transaction unspent again.-unspendOutputs :: Tx -> ImportM ()-unspendOutputs = mapM_ unspendOutput . prevOuts+unspendOutputs :: Writer -> Tx -> IO ()+unspendOutputs wr = mapM_ (unspendOutput wr) . prevOuts  -- | Make an output unspent again.-unspendOutput :: OutPoint -> ImportM ()-unspendOutput op = do-  net <- getNetwork-  ctx <- getCtx+unspendOutput :: Writer -> OutPoint -> IO ()+unspendOutput wr op = do+  net <- getNetwork wr+  ctx <- getCtx wr   t <--    getActiveTxData op.hash >>= \case+    getActiveTxData wr op.hash >>= \case       Nothing -> error $ "Could not find tx data: " <> show op.hash       Just t -> return t   let o =@@ -885,38 +922,39 @@             outpoint = op,             address = m           }-  deleteSpender op-  insertUnspent u+  deleteSpender wr op+  insertUnspent wr u   forM_ m $ \a -> do-    insertAddrUnspent a u-    increaseBalance (confirmed u.block) a o.value+    insertAddrUnspent wr a u+    increaseBalance wr (confirmed u.block) a o.value  -- | Change the total received by an address.-modifyReceived :: Address -> (Word64 -> Word64) -> ImportM ()-modifyReceived a f = do-  b <- getDefaultBalance a-  setBalance b {received = f b.received}+modifyReceived :: Writer -> Address -> (Word64 -> Word64) -> IO ()+modifyReceived wr a f = do+  b <- getDefaultBalance wr a+  setBalance wr b {received = f b.received}  -- | Remove an output from the confirmed or unconfirmed balance of an address.-decreaseBalance :: Bool -> Address -> Word64 -> ImportM ()-decreaseBalance conf = modBalance conf False+decreaseBalance :: Writer -> Bool -> Address -> Word64 -> IO ()+decreaseBalance wr conf = modBalance wr conf False  -- | Add an output to the confirmed or unconfirmed balance of an address.-increaseBalance :: Bool -> Address -> Word64 -> ImportM ()-increaseBalance conf = modBalance conf True+increaseBalance :: Writer -> Bool -> Address -> Word64 -> IO ()+increaseBalance wr conf = modBalance wr conf True  -- | Add or remove an output from the confirmed or unconfirmed balance of an address, and its count of unspent outputs. modBalance ::+  Writer ->   -- | confirmed   Bool ->   -- | add   Bool ->   Address ->   Word64 ->-  ImportM ()-modBalance conf add a val = do-  b <- getDefaultBalance a-  setBalance $ (g . f) b+  IO ()+modBalance wr conf add a val = do+  b <- getDefaultBalance wr a+  setBalance wr $ (g . f) b   where     g b = (b :: Balance) {utxo = m 1 b.utxo}     f b@@ -927,10 +965,10 @@       | otherwise = subtract  -- | Increase or decrease the number of transactions of an address.-modAddressCount :: Bool -> Address -> ImportM ()-modAddressCount add a = do-  b <- getDefaultBalance a-  setBalance b {txs = f b.txs}+modAddressCount :: Writer -> Bool -> Address -> IO ()+modAddressCount wr add a = do+  b <- getDefaultBalance wr a+  setBalance wr b {txs = f b.txs}   where     f       | add = (+ 1)@@ -960,5 +998,5 @@ prevOuts tx = filter (/= nullOutPoint) (map (.outpoint) tx.inputs)  -- | Whether a transaction is in the database and not deleted.-testPresent :: (StoreReadBase m) => Tx -> m Bool-testPresent tx = isJust <$> getActiveTxData (txHash tx)+testPresent :: Writer -> Tx -> IO Bool+testPresent wr tx = isJust <$> getActiveTxData wr (txHash tx)
+ src/Haskoin/Store/Logic/Layered.hs view
@@ -0,0 +1,792 @@+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}++-- |+-- Module      : Haskoin.Store.Logic.Layered+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Rules to import and revert blocks and transactions in the layered layout+-- of the database (@docs\/layered-database.md@), in which the mempool is+-- kept apart from the chain. These are the rules that the block store+-- imports with. They do what "Haskoin.Store.Logic" did for the layout of+-- version 20, with the same functions, the same results and the same+-- errors, which is how the block store moved from one to the other, and+-- how the tests compare the two.+--+-- = Two sets of rules+--+-- __The chain__ has one door. 'importBlock' and 'revertBlock' are the only+-- functions here that write to the column families of the chain, and what+-- they write depends on the block and on those column families alone. A+-- block is imported the same way whether or not the store had seen its+-- transactions before, as during the initial sync: every output it spends+-- must be unspent in the chain, or the block is refused.+--+-- __The mempool__ is a set of transactions, each with its keys in the+-- mempool area ('mempoolRecords'). A transaction enters by having its keys+-- written and leaves by having them deleted; nothing else is read or+-- changed, and nothing is written to the chain. What is left of the rules+-- is the checks: that the outputs a transaction spends exist and are+-- unspent, in the chain or in the mempool, and what it replaces.+--+-- The two meet in three places. A block takes the transactions it confirms+-- out of the mempool, and deletes those that conflict with it. A reverted+-- block gives its transactions back to the mempool. And a mempool+-- transaction may spend outputs of the chain, which the chain goes on+-- listing as unspent: only the mempool area says otherwise.+--+-- = Same answers as version 20+--+-- For any sequence of blocks and transactions, these rules leave the+-- database that "Haskoin.Store.Logic" followed by+-- "Haskoin.Store.Database.Migrate" leaves, key for key, and each step gives+-- the same result. The tests check that for random sequences. There is one+-- difference by design. A block that spends an output of an unconfirmed+-- transaction is refused here, because that output is not in the chain;+-- version 20 imports it, since its indices do not tell the two apart.+module Haskoin.Store.Logic.Layered+  ( initBest,+    revertBlock,+    importBlock,+    newMempoolTx,+    deleteUnconfirmedTx,+  )+where++import Control.Monad+  ( forM,+    forM_,+    guard,+    unless,+    void,+    when,+    zipWithM_,+  )+import Data.ByteString qualified as B+import Data.Either (rights)+import Data.Functor ((<&>))+import Data.HashMap.Strict qualified as M+import Data.HashSet qualified as H+import Data.IntMap.Strict qualified as I+import Data.List (nub)+import Data.Maybe+  ( catMaybes,+    fromMaybe,+    isJust,+    isNothing,+    mapMaybe,+  )+import Data.Serialize (encode)+import Data.Text (Text)+import Data.Word (Word32, Word64)+import Haskoin+  ( Address,+    Block (..),+    BlockHash,+    BlockHeader (..),+    BlockNode (..),+    Ctx,+    Network (..),+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    blockHashToHex,+    computeSubsidy,+    eitherToMaybe,+    genesisBlock,+    genesisNode,+    headerHash,+    isGenesis,+    nullOutPoint,+    scriptToAddressBS,+    txHash,+    txHashToHex,+  )+import Haskoin.Node (LogLevel (..), logAt)+import Haskoin.Store.Common (nub', sortTxs)+import Haskoin.Store.Data+import Haskoin.Store.Database.Mempool (MemRecords (..), mempoolRecords)+import Haskoin.Store.Database.Types (MemAddrKey (..), MemAddrVal (..), MemSummary (..))+import Haskoin.Store.Database.Writer+import Haskoin.Store.Logic (ImportException (..))+import UnliftIO (throwIO)++-------------------------------------------------------------------------------+-- Reading+-------------------------------------------------------------------------------++-- | Log a message with the logger of the writer.+logDebug, logError :: Writer -> Text -> IO ()+logDebug wr = logAt LevelDebug (writerLogger wr)+logError wr = logAt LevelError (writerLogger wr)++-- | Address of an output script, if it has one on the network.+scriptAddress :: Writer -> B.ByteString -> Maybe Address+scriptAddress wr = eitherToMaybe . scriptToAddressBS (writerNet wr) (writerCtx wr)++-- | Data of a confirmed transaction, with the spenders of its outputs that+-- are in blocks. Version 20 kept the bodies of deleted transactions in the+-- same place; they do not count.+chainTx :: Writer -> TxHash -> IO (Maybe TxData)+chainTx wr h =+  getTxData wr h <&> \case+    Just td | not td.deleted -> Just td+    _ -> Nothing++-- | Data of a transaction that is in the mempool or in the chain, without+-- the mempool spenders of its outputs.+activeBody :: Writer -> TxHash -> IO (Maybe TxData)+activeBody wr h = getMempoolTx wr h >>= maybe (chainTx wr h) (return . Just)++-- | Data of a transaction that is in the mempool or in the chain, with every+-- spender of its outputs, in blocks and in the mempool.+activeTx :: Writer -> TxHash -> IO (Maybe TxData)+activeTx wr h = activeBody wr h >>= mapM spent+  where+    spent td = do+      ms <- getMempoolSpenders wr h (length td.tx.outputs)+      -- those of blocks are under keys of their own, and only a confirmed+      -- transaction can have any+      cs <- if confirmed td.block then getChainSpenders wr h (length td.tx.outputs) else return I.empty+      return (td :: TxData) {spenders = I.union cs ms}++-- | An output that a new mempool transaction may spend: one that the chain+-- has as unspent, or one of a mempool transaction, that no mempool+-- transaction spends.+spendable :: Writer -> OutPoint -> IO (Maybe Unspent)+spendable wr p =+  getMempoolSpender wr p >>= \case+    Just _ -> return Nothing+    Nothing ->+      getUnspent wr p >>= \case+        Just u -> return (Just u)+        Nothing -> (>>= output) <$> getMempoolTx wr p.hash+  where+    output td = unspentOutput wr td.block p <$> getTxOut p.index td.tx++-- | An output as an unspent output, given the position of its transaction.+unspentOutput :: Writer -> BlockRef -> OutPoint -> TxOut -> Unspent+unspentOutput wr br p o =+  Unspent+    { block = br,+      outpoint = p,+      value = o.value,+      script = o.script,+      address = scriptAddress wr o.script+    }++-- | Input that spends an output, in the mempool or in a block.+spenderOf :: Writer -> OutPoint -> IO (Maybe Spender)+spenderOf wr p = getMempoolSpender wr p >>= maybe (getChainSpender wr p) (return . Just)++-- | Data of the best block. Throws 'BestBlockNotFound' if there is none.+bestBlockData :: Writer -> IO BlockData+bestBlockData wr = do+  h <-+    getBestBlock wr >>= \case+      Nothing -> do+        logError wr "Best block unknown"+        throwIO BestBlockNotFound+      Just h -> return h+  getBlock wr h >>= \case+    Nothing -> do+      logError wr "Best block not found"+      throwIO BestBlockNotFound+    Just b -> return b++-------------------------------------------------------------------------------+-- Blocks+-------------------------------------------------------------------------------++-- | Import the genesis block if the database has no best block yet.+initBest :: Writer -> IO ()+initBest wr = do+  logDebug wr "Initializing best block"+  let net = writerNet wr+  m <- getBestBlock wr+  when (isNothing m) . void $ do+    logDebug wr "Importing Genesis block"+    importBlock wr (genesisBlock net) (genesisNode net)++-- | Check that a block builds on the best block, or that it is the genesis block+-- of an empty database.+checkNewBlock :: Writer -> Block -> BlockNode -> IO ()+checkNewBlock wr b n =+  getBestBlock wr >>= \case+    Nothing+      | isGenesis n -> return ()+      | otherwise -> do+          logError wr $ "Cannot import non-genesis block: " <> blockHashToHex (headerHash b.header)+          throwIO BestBlockNotFound+    Just h+      | b.header.prev == h -> return ()+      | otherwise -> do+          logError wr $ "Block does not build on head: " <> blockHashToHex (headerHash b.header)+          throwIO PrevBlockNotBest++-- | Import a block that builds on the best block, and make it the best block.+-- Returns its data and the data of its transactions.+--+-- The block goes into the chain's column families as it would into those of+-- a store with an empty mempool. Before that, the mempool gives way: the+-- transactions of the block that were in it leave it, and the mempool+-- transactions that spend an output that the block spends are deleted, with+-- their descendants.+importBlock :: Writer -> Block -> BlockNode -> IO (BlockData, [TxData])+importBlock wr b n = do+  checkNewBlock wr b n+  let net = writerNet wr+      subsidy = computeSubsidy net n.height+  bs <- getBlocksAtHeight wr n.height+  setBlocksAtHeight wr (nub (headerHash n.header : bs)) n.height+  setBest wr (headerHash n.header)+  tds <- importTxs wr n b.txs+  let bd =+        BlockData+          { height = n.height,+            main = True,+            work = n.work,+            header = n.header,+            size = fromIntegral (B.length (encode b)),+            txs = map txHash b.txs,+            weight = if net.segWit then w else 0,+            subsidy = subsidy,+            fee = sum $ map txDataFee tds,+            outputs = ts_out_val+          }+  insertBlock wr bd+  logDebug wr $ "Finished importing block: " <> blockHashToHex (headerHash n.header)+  return (bd, tds)+  where+    ts_out_val =+      sum $ map (sum . map (.value) . (.outputs)) (drop 1 b.txs)+    w =+      let f t = (t :: Tx) {witness = []}+          b' = (b :: Block) {txs = map f b.txs}+          x = B.length (encode b)+          s = B.length (encode b')+       in fromIntegral $ s * 3 + x++-- | Import the transactions of a block, in dependency order, after deleting+-- the mempool transactions that conflict with them.+importTxs :: Writer -> BlockNode -> [Tx] -> IO [TxData]+importTxs wr bn txns = do+  mapM_ (freeOutputs . snd) (reverse txs)+  mapM (uncurry action) txs+  where+    txs = sortTxs txns+    br i = BlockRef {height = bn.height, position = i}+    blockTime = fromIntegral bn.header.timestamp+    freeOutputs tx = getConflicts wr True False tx >>= mapM_ (deleteSingleTx wr . txHash)+    action i tx =+      getMempoolTx wr (txHash tx) >>= \case+        Just td -> do+          -- it leaves the mempool, and with it the time at which this+          -- store first saw it: in the chain its time is that of its+          -- block, like that of a transaction that was never in the+          -- mempool, so that the record is the same on every store+          removeMempoolTx wr td+          td' <- importChainTx wr (br i) blockTime tx+          updateSpendersRBF wr tx+          return td'+        Nothing ->+          chainTx wr (txHash tx) >>= \case+            Just td -> moveChainTx wr td (br i) blockTime+            Nothing -> importChainTx wr (br i) blockTime tx++-- | Import a transaction of a block into the chain, with the time of the+-- block. Throws 'Orphan' if an output that it spends is not unspent in the+-- chain.+--+-- = Why the time of the block+--+-- What the chain's column families hold is to follow from the chain and+-- from nothing else: the same bytes on every store that has the same+-- blocks, whatever each of them saw before. The time of a transaction was+-- the one thing in them that did not. A store kept the time at which it+-- first saw a transaction in its mempool when a block confirmed it, so two+-- stores had two times for it, a store that synced later had the time of+-- the block, and the stores of a fleet gave different answers about the+-- same confirmed transaction. The time of first sight is still what a+-- transaction has while it is in the mempool, where it is the store's own,+-- or the pool's in a fleet.+importChainTx :: Writer -> BlockRef -> Word64 -> Tx -> IO TxData+importChainTx wr br time tx = do+  us <- forM (prevOuts tx) $ \p ->+    getUnspent wr p >>= \case+      Just u -> return u+      Nothing -> do+        logError wr $ "Attempted to import a tx missing UTXO: " <> txHashToHex (txHash tx)+        throwIO Orphan+  let td = prepareTxData False br time tx us+  addChainTx wr td+  return td++-- | Give a confirmed transaction a new position in the chain: a block has a+-- transaction that is already confirmed. Only two coinbases of 2010 are, on+-- Bitcoin; the later one takes the place of the earlier, with the outputs+-- that are still unspent, as in the layout of version 20, and the time of+-- the later block.+moveChainTx :: Writer -> TxData -> BlockRef -> Word64 -> IO TxData+moveChainTx wr t new time = do+  forM_ (txDataAddresses wr t) $ \a -> do+    deleteAddrTx wr a TxRef {block = t.block, txid = h}+    insertAddrTx wr a TxRef {block = new, txid = h}+  forM_ (zip [0 ..] t.tx.outputs) $ \(i, _) ->+    getUnspent wr (OutPoint h i) >>= mapM_ move+  let td = (t :: TxData) {block = new, timestamp = time}+  insertTx wr td+  return td+  where+    h = txHash t.tx+    move u = do+      let u' = (u :: Unspent) {block = new}+      insertUnspent wr u'+      forM_ u.address $ \a -> deleteAddrUnspent wr a u >> insertAddrUnspent wr a u'++-- | Revert the best block: make its parent the best block, mark it as not in+-- the main chain, take its transactions out of the chain and give them to+-- the mempool, and delete its coinbase with whatever spends it. Throws+-- 'BlockNotBest' if the block is not the best block.+revertBlock :: Writer -> BlockHash -> IO ()+revertBlock wr bh = do+  bd <-+    bestBlockData wr >>= \b ->+      if headerHash b.header == bh+        then return b+        else do+          logError wr $ "Cannot revert non-head block: " <> blockHashToHex bh+          throwIO BlockNotBest+  tds <- mapM inChain bd.txs+  setBest wr bd.header.prev+  insertBlock wr bd {main = False}+  case tds of+    [] -> return ()+    coinbase : rest -> do+      let returned = M.fromList [(txHash td.tx, td) | td <- rest]+          -- the transactions that go back to the mempool, each after the+          -- transactions that it spends+          ordered = filter ((`M.member` returned) . txHash) (map snd (sortTxs (map (.tx) tds)))+      -- out of the chain, each after the transactions that spend it+      forM_ (reverse ordered) (leave . txHash)+      -- into the mempool, at the time that each had in the chain, which is+      -- that of the block: the time at which a store first saw it is not+      -- kept past its confirmation, and every store gives it the same+      -- time this way. The coinbase is still in the chain: a transaction that spends it has all its+      -- parents while its flag is worked out, as in the layout of version 20+      forM_ (mapMaybe ((`M.lookup` returned) . txHash) ordered) $ \td -> do+        rbf <- isRBF wr td.tx+        addMempoolTx wr td.timestamp (td :: TxData) {rbf = rbf}+        updateSpendersRBF wr td.tx+      -- a coinbase cannot be in the mempool: it is deleted, and so is+      -- everything that spends it+      spenders <- getMempoolSpenders wr (txHash coinbase.tx) (length coinbase.tx.outputs)+      forM_ (nub' (map (.txid) (I.elems spenders))) $ \h ->+        getChain wr True False h >>= mapM_ (deleteSingleTx wr . txHash)+      -- and forgotten: nothing is kept of a transaction that leaves the+      -- store unconfirmed+      leave (txHash coinbase.tx)+  where+    inChain h =+      chainTx wr h >>= \case+        Just td -> return td+        Nothing -> do+          logDebug wr $ "Tx not found: " <> txHashToHex h+          throwIO TxNotFound+    -- take a transaction that nothing in the chain spends out of the chain+    leave h = do+      td <- inChain h+      spent <- getChainSpenders wr h (length td.tx.outputs)+      unless (I.null spent) $ do+        logError wr $ "Tried to revert spent tx: " <> txHashToHex h+        throwIO TxSpent+      removeChainTx wr td++-------------------------------------------------------------------------------+-- The chain's column families+-------------------------------------------------------------------------------++-- | Data of a new transaction, from its replace-by-fee flag, position, time and+-- the outputs it spends.+prepareTxData :: Bool -> BlockRef -> Word64 -> Tx -> [Unspent] -> TxData+prepareTxData rbf br tt tx us =+  TxData+    { block = br,+      tx = tx,+      prevs = ps,+      deleted = False,+      rbf = rbf,+      timestamp = tt,+      spenders = I.empty+    }+  where+    mkprv Unspent {..} = Prev script value+    ps = I.fromList $ zip [0 ..] $ map mkprv us++-- | Change the balance of an address in the chain. An address whose balance+-- goes back to nothing, as when the only block that paid it is reverted, has+-- none.+modifyBalance :: Writer -> Address -> (Balance -> Balance) -> IO ()+modifyBalance wr a f = do+  b <- f . fromMaybe (zeroBalance a) <$> getBalance wr a+  if nullBalance b then deleteBalance wr a else setBalance wr b++-- | Subtract, or stop: no count or amount of the chain goes below zero.+less :: String -> Word64 -> Word64 -> Word64+less what x y+  | y > x = error $ "The " <> what <> " of an address would go below zero"+  | otherwise = x - y++-- | Add a confirmed transaction to every index of the chain. Every output+-- that it spends must be unspent in the chain.+addChainTx :: Writer -> TxData -> IO ()+addChainTx wr td = do+  forM_ (txDataAddresses wr td) $ \a -> do+    insertAddrTx wr a TxRef {block = td.block, txid = h}+    modifyBalance wr a $ \b -> (b :: Balance) {txs = b.txs + 1}+  forM_ (zip [0 ..] td.tx.outputs) $ \(i, o) -> do+    let u = unspentOutput wr td.block (OutPoint h i) o+    insertUnspent wr u+    forM_ u.address $ \a -> do+      insertAddrUnspent wr a u+      modifyBalance wr a $ \b ->+        (b :: Balance)+          { confirmed = b.confirmed + o.value,+            utxo = b.utxo + 1,+            received = b.received + o.value+          }+  zipWithM_ spend [0 ..] (prevOuts td.tx)+  insertTx wr td+  where+    h = txHash td.tx+    spend ix p = do+      u <-+        getUnspent wr p >>= \case+          Just u -> return u+          Nothing -> error $ "Could not find UTXO to spend: " <> show p+      deleteUnspent wr p+      -- under the key of the output: the record of the transaction that+      -- made it is written once, when it is confirmed, and is not read+      -- here either+      insertSpender wr p (Spender h ix)+      forM_ u.address $ \a -> do+        deleteAddrUnspent wr a u+        modifyBalance wr a $ \b ->+          (b :: Balance)+            { confirmed = less "confirmed balance" b.confirmed u.value,+              utxo = less "count of unspent outputs" b.utxo 1+            }++-- | Take a confirmed transaction that nothing in the chain spends out of+-- every index of the chain, and make the outputs that it spent unspent+-- again.+removeChainTx :: Writer -> TxData -> IO ()+removeChainTx wr td = do+  forM_ (txDataAddresses wr td) $ \a -> do+    deleteAddrTx wr a TxRef {block = td.block, txid = h}+    modifyBalance wr a $ \b -> (b :: Balance) {txs = less "count of transactions" b.txs 1}+  forM_ (zip [0 ..] td.tx.outputs) $ \(i, o) -> do+    let p = OutPoint h i+        u = unspentOutput wr td.block p o+    deleteUnspent wr p+    forM_ u.address $ \a -> do+      deleteAddrUnspent wr a u+      modifyBalance wr a $ \b ->+        (b :: Balance)+          { confirmed = less "confirmed balance" b.confirmed o.value,+            utxo = less "count of unspent outputs" b.utxo 1,+            received = less "amount received" b.received o.value+          }+  forM_ (prevOuts td.tx) $ \p -> do+    parent <-+      chainTx wr p.hash >>= \case+        Just t -> return t+        Nothing -> error $ "Could not find tx data: " <> show p.hash+    let o = fromMaybe (error ("Could not find output: " <> show p)) (getTxOut p.index parent.tx)+        u = unspentOutput wr parent.block p o+    deleteSpender wr p+    insertUnspent wr u+    forM_ u.address $ \a -> do+      insertAddrUnspent wr a u+      modifyBalance wr a $ \b -> (b :: Balance) {confirmed = b.confirmed + o.value, utxo = b.utxo + 1}+  deleteTxData wr h+  where+    h = txHash td.tx++-------------------------------------------------------------------------------+-- The mempool area+-------------------------------------------------------------------------------++-- | Import a transaction into the mempool with the time it arrived. Returns+-- 'False' if it is already known.+--+-- Conflicting unconfirmed transactions are deleted together with their+-- descendants if the network has replace-by-fee; otherwise the transaction is+-- rejected with 'DoubleSpend'. Throws an 'ImportException' if the transaction is+-- invalid, for example if it spends unknown outputs.+newMempoolTx :: Writer -> Tx -> UnixTime -> IO Bool+newMempoolTx wr tx w =+  activeBody wr (txHash tx) >>= \case+    Just _ -> return False+    Nothing -> do+      -- Check everything before writing, so that a rejected transaction+      -- leaves the mempool as it was.+      replaced <- getConflicts wr True True tx+      checkNewTx wr (map txHash replaced) tx+      mapM_ (deleteSingleTx wr . txHash) replaced+      rbf <- isRBF wr tx+      us <- forM (prevOuts tx) $ \p ->+        spendable wr p >>= \case+          Just u -> return u+          Nothing -> do+            logError wr $ "Attempted to import a tx missing UTXO: " <> txHashToHex (txHash tx)+            throwIO Orphan+      addMempoolTx wr w (prepareTxData rbf (MemRef w) w tx us)+      return True++-- | Delete an unconfirmed transaction and its descendants. If the flag is set and+-- the network has no replace-by-fee, throw 'DoubleSpend' instead. Does nothing if+-- the transaction is not found.+deleteUnconfirmedTx :: Writer -> Bool -> TxHash -> IO ()+deleteUnconfirmedTx wr rbfcheck th =+  activeBody wr th >>= \case+    Just _ -> getChain wr True rbfcheck th >>= mapM_ (deleteSingleTx wr . txHash)+    Nothing -> logDebug wr $ "Not found or already deleted: " <> txHashToHex th++-- | Write the keys of a transaction to the mempool area, with the time it+-- entered the mempool. The body goes in at that mempool position and without+-- spenders, whatever it had.+addMempoolTx :: Writer -> UnixTime -> TxData -> IO ()+addMempoolTx wr time td = do+  insertMempoolTx wr r.body+  forM_ r.spends (uncurry (insertMempoolSpender wr))+  forM_ r.addresses (uncurry (insertMempoolAddr wr))+  adjustSummaries wr (+) r+  addToMempool wr r.txid r.time+  where+    r = mempoolRecords (writerNet wr) (writerCtx wr) time td++-- | Delete the keys of a mempool transaction, given its body, from the+-- mempool area.+removeMempoolTx :: Writer -> TxData -> IO ()+removeMempoolTx wr td = do+  deleteMempoolTx wr r.txid+  forM_ r.spends (deleteMempoolSpender wr . fst)+  forM_ r.addresses (deleteMempoolAddr wr . fst)+  adjustSummaries wr (less "mempool summary") r+  deleteFromMempool wr r.txid+  where+    time = case td.block of+      MemRef t -> t+      BlockRef {} -> td.timestamp+    r = mempoolRecords (writerNet wr) (writerCtx wr) time td++-- | Add what a transaction does at each address it touches to the summary+-- of the address, or take it off again, given how to combine a number of+-- the summary with one of the transaction.+--+-- Everything is in the entries of the transaction but one thing: whether an+-- output that it spends was made by the mempool, and whether an output that+-- it makes is spent by the mempool, which is what 'both' counts. Each is a+-- read by key of what another transaction wrote. An output is counted in+-- 'both' while both of its transactions are in the mempool, so it is added+-- by the second of them to arrive and taken off by the first to leave,+-- whichever that is.+adjustSummaries :: Writer -> (Word64 -> Word64 -> Word64) -> MemRecords -> IO ()+adjustSummaries wr op r = do+  deltas <- forM r.addresses $ \case+    (MemAddrTxKey a _ _, _) -> return (a, MemSummary 1 0 0 0 0 0 0)+    (MemAddrSpentKey a p, MemAddrSpentVal v) -> do+      made <- inMempool wr p.hash+      return (a, if made then MemSummary 0 0 0 v 1 v 1 else MemSummary 0 0 0 v 1 0 0)+    (MemAddrOutKey a _ p, MemAddrOutVal v _) -> do+      taken <- isJust <$> getMempoolSpender wr p+      return (a, if taken then MemSummary 0 v 1 0 0 v 1 else MemSummary 0 v 1 0 0 0 0)+    (k, _) -> error ("Not an entry of a transaction: " <> show k)+  forM_ (M.toList (M.fromListWith plus deltas)) $ \(a, d) -> do+    old <-+      getMempoolAddr wr (MemAddrSumKey a) <&> \case+        Just (MemAddrSumVal s) -> s+        _ -> zero+    let new = combine op old d+    if new == zero+      then deleteMempoolAddr wr (MemAddrSumKey a)+      else insertMempoolAddr wr (MemAddrSumKey a) (MemAddrSumVal new)+  where+    zero = MemSummary 0 0 0 0 0 0 0+    plus = combine (+)+    combine f x y =+      MemSummary+        { txs = f x.txs y.txs,+          paid = f x.paid y.paid,+          paidCount = f x.paidCount y.paidCount,+          spent = f x.spent y.spent,+          spentCount = f x.spentCount y.spentCount,+          both = f x.both y.both,+          bothCount = f x.bothCount y.bothCount+        }++-- | Delete a mempool transaction that nothing spends. Nothing is kept of+-- it. Throws 'TxSpent' if something spends it.+--+-- Until version 22 its body was kept, marked as deleted, and the API+-- returned it with that mark. Clients that did not read the mark took a+-- replaced transaction for a payment, each store had its own set of them,+-- which a fleet cannot answer from, and nothing ever removed them. What+-- the store needs of them, not to fetch a transaction again that it just+-- deleted, it remembers for a while in memory+-- ("Haskoin.Store.BlockStore").+deleteSingleTx :: Writer -> TxHash -> IO ()+deleteSingleTx wr th =+  getMempoolTx wr th >>= \case+    Nothing -> do+      logError wr $ "Already deleted: " <> txHashToHex th+      throwIO TxNotFound+    Just td -> do+      spenders <- getMempoolSpenders wr th (length td.tx.outputs)+      if I.null spenders+        then do+          logDebug wr $ "Deleting tx: " <> txHashToHex th+          removeMempoolTx wr td+        else do+          logError wr $ "Tried to delete spent tx: " <> txHashToHex th+          throwIO TxSpent++-- | Check a transaction that will replace these ones. Outputs that they spend+-- count as unspent, and outputs that they create as missing.+checkNewTx :: Writer -> [TxHash] -> Tx -> IO ()+checkNewTx wr replaced tx = do+  when (unique_inputs < length tx.inputs) $ do+    logError wr $ "Transaction spends same output twice: " <> txHashToHex (txHash tx)+    throwIO DuplicatePrevOutput+  prevs <- mapM prev_output (prevOuts tx)+  when (any isNothing prevs) $ do+    logError wr $ "Orphan: " <> txHashToHex (txHash tx)+    throwIO Orphan+  when (isCoinbaseTx tx) $ do+    logError wr $ "Coinbase cannot be imported into mempool: " <> txHashToHex (txHash tx)+    throwIO UnexpectedCoinbase+  when (outputs > sum (map (.value) (catMaybes prevs))) $ do+    logError wr $ "Insufficient funds for tx: " <> txHashToHex (txHash tx)+    throwIO InsufficientFunds+  where+    prev_output op+      | op.hash `elem` replaced = return Nothing+      | otherwise = (>>= getTxOut op.index . (.tx)) <$> activeBody wr op.hash+    outputs = sum (map (.value) tx.outputs)+    unique_inputs = length (nub' (map (.outpoint) tx.inputs))++-- | Transactions that spend the same outputs as this one, with their+-- descendants, in an order in which they can be deleted. The first flag+-- refuses confirmed transactions, and the second refuses any on networks+-- without replace-by-fee.+getConflicts :: Writer -> Bool -> Bool -> Tx -> IO [Tx]+getConflicts wr memonly rbfcheck tx = do+  let prevs = prevOuts tx+  unspents <- mapM (spendable wr) prevs+  let spents = [p | (p, Nothing) <- zip prevs unspents]+  spndrs <- catMaybes <$> mapM (spenderOf wr) spents+  let txids = H.fromList $ filter (/= txHash tx) $ map (.txid) spndrs+  chains <- mapM (getChain wr memonly rbfcheck) (H.toList txids)+  return . reverse . map snd . sortTxs . M.elems $+    M.fromList [(txHash t, t) | t <- concat chains]++-- | A transaction and all its descendants, in an order in which they can be+-- deleted: spenders first.+getChain :: Writer -> Bool -> Bool -> TxHash -> IO [Tx]+getChain wr memonly rbfcheck th' =+  sort_clean <$> go M.empty (H.singleton th')+  where+    sort_clean = reverse . map snd . sortTxs+    get_tx th =+      activeTx wr th >>= \case+        Nothing -> do+          logDebug wr $ "Transaction not found: " <> txHashToHex th+          return Nothing+        Just td+          | memonly && confirmed td.block -> do+              logError wr $ "Transaction already confirmed: " <> txHashToHex th+              throwIO TxConfirmed+          | rbfcheck ->+              if (writerNet wr).replaceByFee+                then return $ Just td+                else do+                  logError wr $ "Double-spending transaction: " <> txHashToHex th+                  throwIO DoubleSpend+          | otherwise -> return $ Just td+    go txm pdg = do+      let ths = filter (not . (`M.member` txm)) (H.toList pdg)+      tds <- catMaybes <$> mapM get_tx ths+      let txmn = M.fromList $ fmap (\d -> (txHash d.tx, d.tx)) tds+          spds = concatMap (map (.txid) . I.elems . (.spenders)) tds+          pdg' = H.fromList spds+          txm' = txmn <> txm+      if H.null pdg'+        then return $ M.elems txm'+        else go txm' pdg'++-- | Whether a mempool transaction can be replaced by fee: it signals BIP125+-- in one of its inputs, or it spends an unconfirmed transaction that can be+-- replaced. Always 'False' on networks without replace-by-fee.+isRBF :: Writer -> Tx -> IO Bool+isRBF wr tx+  | not (writerNet wr).replaceByFee = return False+  | any ((< 0xffffffff - 1) . (.sequence)) tx.inputs = return True+  | otherwise = ck (nub' $ map (.outpoint.hash) tx.inputs)+  where+    ck [] = return False+    ck (h : hs') =+      activeBody wr h >>= \case+        Nothing -> return False+        Just t+          | confirmed t.block -> ck hs'+          | t.rbf -> return True+          | otherwise -> ck hs'++-- | Mempool transactions inherit BIP125 signalling from unconfirmed parents+-- only, so their flag can change when a parent is confirmed or goes back to+-- the mempool. Correct the flags of the mempool transactions that spend the+-- outputs of a transaction, and of theirs in turn.+updateSpendersRBF :: Writer -> Tx -> IO ()+updateSpendersRBF wr tx = do+  spenders <- getMempoolSpenders wr (txHash tx) (length tx.outputs)+  forM_ (nub' (map (.txid) (I.elems spenders))) $ \h ->+    getMempoolTx wr h >>= mapM_ update+  where+    update s = do+      rbf <- isRBF wr s.tx+      when (rbf /= s.rbf) $ do+        insertMempoolTx wr (s :: TxData) {rbf = rbf}+        updateSpendersRBF wr s.tx++-------------------------------------------------------------------------------+-- Transactions+-------------------------------------------------------------------------------++-- | Output of a transaction at an index, or 'Nothing' if out of range.+getTxOut :: Word32 -> Tx -> Maybe TxOut+getTxOut i tx = do+  guard (fromIntegral i < length tx.outputs)+  return $ tx.outputs !! fromIntegral i++-- | Addresses that a transaction is indexed under: those of its outputs and+-- of the outputs that it spends. An output script counts only when it has+-- an address on the network.+txDataAddresses :: Writer -> TxData -> [Address]+txDataAddresses wr t =+  nub' . rights $+    map (scriptToAddressBS net ctx . (.script)) (I.elems t.prevs)+      <> map (scriptToAddressBS net ctx . (.script)) t.tx.outputs+  where+    net = writerNet wr :: Network+    ctx = writerCtx wr :: Ctx++-- | Outputs spent by a transaction, without the null outpoint of a coinbase.+prevOuts :: Tx -> [OutPoint]+prevOuts tx = filter (/= nullOutPoint) (map (.outpoint) tx.inputs)
src/Haskoin/Store/Manager.hs view
@@ -1,10 +1,3 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE NoFieldSelectors #-}- -- | -- Module      : Haskoin.Store.Manager -- Copyright   : (c) Haskoin Developers@@ -15,11 +8,12 @@ -- -- Wires the components of a store together: the RocksDB database, the -- peer-to-peer node, the block store that imports data, and the optional Redis--- cache with its writer process. Node events are forwarded to the block store--- and turned into 'StoreEvent's.+-- cache. Node events are forwarded to the block store and turned into+-- 'StoreEvent's, which the cache follows.+-- -- Keeping the wiring in one place means each component only knows about the--- interfaces it uses: the block store does not know about the cache, and the--- node does not know about either. 'withStore' starts them in dependency order+-- interfaces it uses: the node does not know about the block store, and the+-- block store does not know about the cache. 'withStore' starts them in dependency order -- and stops them all together. module Haskoin.Store.Manager   ( StoreConfig (..),@@ -28,15 +22,14 @@   ) where -import Control.Monad (unless)-import Control.Monad.Cont-  ( ContT (..),-    runContT,-  )-import Control.Monad.Reader (ReaderT (ReaderT), runReaderT)-import Control.Monad.Trans (lift)+import Control.Monad (forM_, unless, when)+import Data.ByteString (ByteString)+import Data.ByteString.Char8 qualified as C+import Data.Serialize (Serialize, decode)+import Data.String.Conversions (cs) import Data.Time.Clock (NominalDiffTime)-import Data.Word (Word32)+import Data.Word (Word32, Word64)+import Database.Redis qualified as Redis import Haskoin   ( BlockHash (..),     Ctx,@@ -57,6 +50,7 @@ import Haskoin.Node   ( Chain,     ChainEvent (..),+    LogLevel (..),     Logger,     Node (..),     NodeConfig (..),@@ -64,6 +58,7 @@     PeerEvent (..),     PeerMgr,     WithConnection,+    logAt,     withNode,     withSource,   )@@ -79,48 +74,46 @@     blockStoreTxSTM,     withBlockStore,   )-import Haskoin.Store.Cache-  ( CacheConfig (..),-    CacheWriter,-    cacheNewBlock,-    cacheNewTx,-    cacheSyncMempool,-    cacheWriter,-    connectRedis,-    newCacheMetrics,-  )+import Haskoin.Store.Cache (Cache, CacheConfig (..), cachePrefix, withCache) import Haskoin.Store.Common   ( StoreEvent (..),     loopForever,   )+import Haskoin.Store.Database.Migrate (Migration (..), upgradeDatabase) import Haskoin.Store.Database.Reader   ( DatabaseReader (..),-    DatabaseReaderT,     createDataMetrics,-    withDatabaseReader,+    withLayeredReader,   )+import Haskoin.Store.Fleet.Member+  ( Member,+    MemberConfig (..),+    memberCacheLoop,+    memberEvents,+    memberForget,+    memberLoop,+    newMember,+    noteBest,+  )+import Haskoin.Store.Fleet.Redis (FleetEvent (..)) import NQE   ( Inbox,-    Process (..),     Publisher,+    publish,     publishSTM,     receive,-    withProcess,     withPublisher,     withSubscription,   ) import Network.Socket (SockAddr (..)) import System.Metrics.StatsD+import System.Random (randomIO) import UnliftIO-  ( MonadIO,-    MonadUnliftIO,-    STM,+  ( STM,     atomically,-    liftIO,     link,     withAsync,   )-import UnliftIO.Concurrent (threadDelay)  -- | Store mailboxes. Handles to the running components of a store, given by -- 'withStore'.@@ -133,8 +126,17 @@     block :: !BlockStore,     -- | database reader     db :: !DatabaseReader,-    -- | cache configuration, if Redis is configured-    cache :: !(Maybe CacheConfig),+    -- | cache of extended public keys, if Redis is configured and the store+    -- is not a member of a fleet+    cache :: !(Maybe Cache),+    -- | the store as a member of a fleet, if it is one+    fleet :: !(Maybe Member),+    -- | what the store tells its clients: new best blocks and changes of+    -- the mempool. For a store alone they are its own, those that the+    -- block store publishes.+    -- For a member of a fleet they are those of the fleet, read from the+    -- Redis that it shares, so that every server tells the same.+    events :: !(Publisher StoreEvent),     -- | store events are published here     pub :: !(Publisher StoreEvent),     -- | network constants@@ -155,7 +157,8 @@     db :: !FilePath,     -- | network constants     net :: !Network,-    -- | Redis cache configuration+    -- | Redis URL for the cache of extended public keys ("Haskoin.Store.Cache");+    -- 'Nothing' for no cache, an empty URL for the local Redis     redis :: !(Maybe String),     -- | Secp256k1 context     ctx :: !Ctx,@@ -163,14 +166,25 @@     initGap :: !Word32,     -- | gap for extended public keys     gap :: !Word32,-    -- | cache xpubs with more than this many used addresses+    -- | cache xpubs with at least this many used addresses     redisMinAddrs :: !Int,-    -- | maximum number of keys in Redis cache-    redisMaxKeys :: !Integer,+    -- | or with at least this many transactions in their addresses+    redisMinTxs :: !Int,+    -- | evict the least recently used xpubs from the cache past this many+    redisMaxXPubs :: !Int,     -- | do not index new mempool transactions     noMempool :: !Bool,     -- | wipe mempool when starting     wipeMempool :: !Bool,+    -- | answer as one of a fleet of stores that share the Redis of 'redis':+    -- every store of the fleet then gives the same answer to the same+    -- request, from the block that they agree on and the mempool that they+    -- share ("Haskoin.Store.Fleet.Member"). The cache of extended public+    -- keys of a store alone ("Haskoin.Store.Cache") is not used then: its+    -- entries follow each store's own database, which is what a fleet does+    -- not answer from. A fleet has one of its own+    -- ("Haskoin.Store.Fleet.XPub"), under the same three settings.+    fleet :: !Bool,     -- | sync mempool from peers     syncMempool :: !Bool,     -- | blocks to request from the syncing peer in one batch@@ -181,13 +195,11 @@     peerTimeout :: !NominalDiffTime,     -- | disconnect peer if it has been connected this long     maxPeerLife :: !NominalDiffTime,-    -- | connect to peers using the function 'withConnection'+    -- | connect to peers using the function 'Haskoin.Node.withConnection'     connect :: !(SockAddr -> WithConnection),     -- | StatsD     stats :: !(Maybe Stats),-    -- | sync mempool against cache every this many seconds-    redisSyncInterval :: !Int,-    -- | database Bloom filters+    -- | Bloom filters on the column families that are read by key     bloom :: !Bool,     -- | RocksDB max_open_files ('Nothing' = unlimited: every SST file's index     -- and filter blocks stay resident for the life of the process)@@ -210,46 +222,151 @@ -- when the action returns, and an exception in any component is re-thrown in -- the calling thread. ----- The database is opened first, then the node, the cache (if 'redis' is set)--- and the block store.+-- The database is opened first, then the node, the block store and the cache+-- (if 'redis' is set). withStore ::-  (MonadUnliftIO m) =>   StoreConfig ->-  (Store -> m a) ->-  m a+  (Store -> IO a) ->+  IO a withStore cfg action =-  connectDB cfg $-    ReaderT $ \db -> flip runContT return $ do-      pub <- ContT withPublisher-      node_pub <- ContT withPublisher-      node_sub <- ContT $ withSubscription node_pub-      node <- ContT $ withNode $ nodeCfg cfg db node_pub-      cache_cfg <- ContT $ withCache cfg node.chain db pub-      block_store <- ContT $ withBlockStore $ blockStoreCfg cfg node pub db-      fwd <- ContT $ withAsync $ nodeForwarder block_store pub node_sub-      link fwd-      lift $-        action-          Store-            { peerMgr = node.peerMgr,-              chain = node.chain,-              block = block_store,-              db = db,-              cache = cache_cfg,-              pub = pub,-              net = cfg.net,-              ctx = cfg.ctx-            }+  connectDB cfg $ \db ->+    withPublisher $ \pub ->+      withPublisher $ \node_pub ->+        withSubscription node_pub $ \node_sub ->+          withNode (nodeCfg cfg db node_pub) $ \node -> do+            member <- storeMember cfg db+            withBlockStore (blockStoreCfg cfg node pub db member) $ \block_store ->+              withAsync (nodeForwarder block_store pub node_sub) $ \fwd -> do+                link fwd+                withStoreFleet cfg pub member . withStoreCache cfg db node.chain pub $ \cache ->+                  withFleetEvents cfg pub member $ \events ->+                    action $+                      Store+                        { peerMgr = node.peerMgr,+                          chain = node.chain,+                          block = block_store,+                          db = db,+                          cache = cache,+                          fleet = member,+                          events = events,+                          pub = pub,+                          net = cfg.net,+                          ctx = cfg.ctx+                        } --- | Open the database described by the configuration and run an action with its reader.+-- | The store as a member of a fleet, if the configuration says that it is+-- one and has a Redis to share: under a name of its own, with a lease of+-- ten seconds, views of its last six blocks, and the store's limit on the+-- age of unconfirmed transactions.+storeMember :: StoreConfig -> DatabaseReader -> IO (Maybe Member)+storeMember cfg db = case (cfg.fleet, cfg.redis) of+  (True, Just url) -> do+    info <-+      if null url+        then return Redis.defaultConnectInfo+        else either fail return (Redis.parseConnectInfo url)+    conn <- Redis.checkedConnect info+    name <- C.pack . show <$> (randomIO :: IO Word64)+    Just+      <$> newMember+        MemberConfig+          { prefix = cachePrefix cfg.net,+            name,+            lease = 10000,+            depth = 6,+            expiry = fromIntegral cfg.mempoolTimeout * 24 * 3600,+            cacheMax = cfg.redisMaxXPubs,+            cacheMinAddrs = cfg.redisMinAddrs,+            cacheMinTxs = cfg.redisMinTxs,+            stats = cfg.stats,+            note = \level -> logAt level (withSource "Fleet" cfg.logger)+          }+        conn+        db+  _ -> return Nothing++-- | Take the store's turns as a member of its fleet while an action runs,+-- if it is one: every two seconds, and whenever the best block changes.+-- Beside them, in a thread of its own, the member builds the entries of+-- the fleet's cache of extended public keys that requests asked for.+withStoreFleet :: StoreConfig -> Publisher StoreEvent -> Maybe Member -> IO a -> IO a+withStoreFleet _ _ Nothing action = action+withStoreFleet cfg pub (Just member) action =+  withSubscription pub $ \sub ->+    withAsync (memberLoop member (nextBlock sub) complain) $ \a ->+      withAsync (memberCacheLoop member cacheComplain) $ \b -> link a >> link b >> action+  where+    -- until the next block, telling the member what the store deletes+    nextBlock sub =+      receive sub >>= \case+        StoreBestBlock _ -> return ()+        StoreMempoolDelete h -> memberForget member [h] >> nextBlock sub+        _ -> nextBlock sub+    complain why = logAt LevelError (withSource "Fleet" cfg.logger) ("Could not take a turn: " <> cs why)+    cacheComplain why = logAt LevelError (withSource "Fleet" cfg.logger) ("Could not cache an xpub: " <> cs why)++-- | Run an action with the events that the store tells its clients. A+-- store alone tells its own. A member of a fleet tells those of the+-- fleet, which a thread reads from Redis for as long as the action runs.+withFleetEvents ::+  StoreConfig ->+  Publisher StoreEvent ->+  Maybe Member ->+  (Publisher StoreEvent -> IO a) ->+  IO a+withFleetEvents _ pub Nothing action = action pub+withFleetEvents cfg _ (Just member) action =+  withPublisher $ \shared ->+    withAsync (memberEvents member False (tell shared) complain) $ \a -> link a >> action shared+  where+    tell shared event = forM_ (told event) (`publish` shared)+    told = \case+      FleetBlock h -> StoreBestBlock <$> hash h+      FleetEntered h -> StoreMempoolNew <$> hash h+      FleetLeft h -> StoreMempoolDelete <$> hash h+    hash :: (Serialize x) => ByteString -> Maybe x+    hash = either (const Nothing) Just . decode+    complain why = logAt LevelError (withSource "Fleet" cfg.logger) ("Could not read the events: " <> cs why)++-- | Run the cache while an action runs, if Redis is configured.+withStoreCache ::+  StoreConfig ->+  DatabaseReader ->+  Chain ->+  Publisher StoreEvent ->+  (Maybe Cache -> IO a) ->+  IO a+withStoreCache cfg db chain pub action =+  case cfg.redis of+    Nothing -> action Nothing+    Just _ | cfg.fleet -> action Nothing+    Just url ->+      withCache+        CacheConfig+          { url = url,+            minAddrs = cfg.redisMinAddrs,+            minTxs = cfg.redisMinTxs,+            maxXPubs = cfg.redisMaxXPubs,+            stats = cfg.stats+          }+        db+        chain+        pub+        (withSource "Cache" cfg.logger)+        (action . Just)++-- | Open the database described by the configuration, bring it to the+-- layout that the store runs on ('upgradeDatabase'), and run an action with+-- its reader. A database of version 20 has its mempool moved to an area of+-- its own here, once, before anything reads it; older stores refuse the+-- database from then on. connectDB ::-  (MonadUnliftIO m) =>   StoreConfig ->-  DatabaseReaderT m a ->-  m a+  (DatabaseReader -> IO a) ->+  IO a connectDB cfg f = do-  stats <- liftIO $ mapM createDataMetrics cfg.stats-  withDatabaseReader+  stats <- mapM createDataMetrics cfg.stats+  withLayeredReader     cfg.net     cfg.ctx     cfg.initGap@@ -261,7 +378,15 @@     cfg.maxWalSize     cfg.blockCache     stats-    f+    $ \db -> do+      done <- upgradeDatabase (logAt LevelInfo (withSource "Database" cfg.logger)) db+      when (done.moved > 0) $+        logAt LevelInfo (withSource "Database" cfg.logger) $+          "Moved " <> cs (show done.moved) <> " mempool transactions to their own area"+      when (done.rewritten > 0) $+        logAt LevelInfo (withSource "Database" cfg.logger) $+          "Wrote the records of " <> cs (show done.rewritten) <> " confirmed transactions again, to be written once"+      f db  -- | Configuration of the block store, from the store configuration. blockStoreCfg ::@@ -269,8 +394,9 @@   Node ->   Publisher StoreEvent ->   DatabaseReader ->+  Maybe Member ->   BlockStoreConfig-blockStoreCfg cfg node pub db =+blockStoreCfg cfg node pub db member =   BlockStoreConfig     { chain = node.chain,       peerMgr = node.peerMgr,@@ -280,6 +406,7 @@       ctx = cfg.ctx,       noMempool = cfg.noMempool,       wipeMempool = cfg.wipeMempool,+      onBest = maybe (\_ -> return ()) noteBest member,       syncMempool = cfg.syncMempool,       blockWindow = cfg.blockWindow,       mempoolTimeout = cfg.mempoolTimeout,@@ -314,82 +441,12 @@       logger = cfg.logger     } --- | If Redis is configured, connect to it, start the cache writer, and pass the--- cache configuration to the action. Otherwise pass 'Nothing'.-withCache ::-  (MonadUnliftIO m) =>-  StoreConfig ->-  Chain ->-  DatabaseReader ->-  Publisher StoreEvent ->-  (Maybe CacheConfig -> m a) ->-  m a-withCache cfg chain db pub action =-  case cfg.redis of-    Nothing ->-      action Nothing-    Just redisurl -> do-      conn <- connectRedis redisurl-      metrics <- mapM (\s -> newCacheMetrics logger s conn db) cfg.stats-      withSubscription pub $ \evts ->-        let conf = c conn metrics-         in withProcess (f conf) $ \p ->-              cacheWriterProcesses-                cfg.redisSyncInterval-                evts-                (getProcessMailbox p)-                (action (Just conf))-  where-    f conf cwinbox = runReaderT (cacheWriter conf cwinbox) db-    logger = withSource "Cache" cfg.logger-    c conn metrics =-      CacheConfig-        { redis = conn,-          minAddrs = cfg.redisMinAddrs,-          chain = chain,-          maxKeys = cfg.redisMaxKeys,-          metrics = metrics,-          logger = logger-        }---- | Run the loop that feeds store events to the cache writer while the action runs.-cacheWriterProcesses ::-  (MonadUnliftIO m) =>-  Int ->-  Inbox StoreEvent ->-  CacheWriter ->-  m a ->-  m a-cacheWriterProcesses interval evts cwm action =-  withAsync (cacheWriterEvents interval evts cwm) $ \a1 -> link a1 >> action---- | Forward store events to the cache writer, and ask it to sync the mempool--- every given number of seconds.-cacheWriterEvents :: (MonadUnliftIO m) => Int -> Inbox StoreEvent -> CacheWriter -> m ()-cacheWriterEvents interval evts cwm =-  withAsync mempool . const $-    loopForever $-      receive evts >>= \e ->-        e `cacheWriterDispatch` cwm-  where-    mempool = loopForever $ do-      threadDelay (interval * 1000 * 1000)-      cacheSyncMempool cwm---- | Tell the cache writer about new blocks and mempool changes.-cacheWriterDispatch :: (MonadIO m) => StoreEvent -> CacheWriter -> m ()-cacheWriterDispatch (StoreBestBlock _) = cacheNewBlock-cacheWriterDispatch (StoreMempoolNew t) = cacheNewTx t-cacheWriterDispatch (StoreMempoolDelete t) = cacheNewTx t-cacheWriterDispatch _ = const (return ())- -- | Forward every node event to the block store and the store publisher, forever. nodeForwarder ::-  (MonadIO m) =>   BlockStore ->   Publisher StoreEvent ->   Inbox NodeEvent ->-  m ()+  IO () nodeForwarder b pub sub =   loopForever $ receive sub >>= atomically . storeDispatch b pub 
src/Haskoin/Store/Web.hs view
@@ -1,3077 +1,3567 @@ {-# LANGUAGE BlockArguments #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}-{-# OPTIONS_GHC -Wno-deprecations #-}---- |--- Module      : Haskoin.Store.Web--- Copyright   : (c) Haskoin Developers--- License     : MIT--- Maintainer  : jprupp@protonmail.ch--- Stability   : experimental--- Portability : POSIX------ REST API server of Haskoin Store, built with Scotty.------ 'runWeb' serves the endpoints described in "Haskoin.Store.WebCommon" and a--- set of endpoints compatible with the Blockchain.info API under--- @\/blockchain\/@. Responses are JSON by default, pretty-printed with--- @?pretty=true@, or binary if the request has the header--- @Accept: application\/octet-stream@. New blocks and mempool transactions are--- streamed at @\/events@, as lines of JSON or over a WebSocket.------ Limits on the size of responses are set with 'WebLimits'. Errors are--- returned as JSON with an HTTP status that depends on the 'Except'.--- = Design of the API------ * __Two encodings of the same data.__ JSON is easy to use from anywhere. The---   binary encoding is several times smaller and faster to produce and parse, and---   is what the Haskell client uses.------ * __Limits protect the server.__ A single request for every transaction of a---   busy address could take minutes and gigabytes. 'WebLimits' caps list sizes---   and offsets, and lower caps apply to full transactions, which are much larger---   than references.------ * __Health is measured, not assumed.__ The store reports itself unhealthy when---   it lags behind the network, stops receiving blocks or transactions, has too---   few peers or a backlog of transactions. Load balancers can route around an---   instance that is still syncing or has lost its peers.------ * __Slow event clients are dropped.__ Each @\/events@ client gets a bounded---   queue. A client that falls too far behind is disconnected rather than allowed---   to make the server buffer events without limit.------ * __Compatibility endpoints ease migration.__ The @\/blockchain\/@ endpoints---   follow the Blockchain.info API so that existing wallets can switch to a---   self-hosted server with few changes.-module Haskoin.Store.Web-  ( -- * Web-    WebConfig (..),-    Except (..),-    WebLimits (..),-    runWeb,--    -- * Request parsing-    decodeTxBody,-  )-where--import Conduit-  ( ConduitT,-    await,-    concatMapC,-    concatMapMC,-    dropC,-    dropWhileC,-    headC,-    mapC,-    runConduit,-    sinkList,-    takeC,-    takeWhileC,-    yield,-    (.|),-  )-import Control.Applicative ((<|>))-import Control.Lens ((.~), (^.))-import Control.Monad-  ( forM_,-    unless,-    void,-    when,-    (<=<),-  )-import Control.Monad.Reader-  ( MonadReader,-    ReaderT,-    asks,-    local,-    runReaderT,-  )-import Control.Monad.Trans (lift)-import Control.Monad.Trans.Class (MonadTrans)-import Control.Monad.Trans.Control-  ( liftWith,-    restoreT,-  )-import Control.Monad.Trans.Maybe-  ( MaybeT (..),-    runMaybeT,-  )-import Data.Aeson-  ( Encoding,-    ToJSON (..),-    Value,-  )-import Data.Aeson qualified as A-import Data.Aeson.Encode.Pretty-  ( Config (..),-    defConfig,-    encodePretty',-  )-import Data.Aeson.Encoding-  ( encodingToLazyByteString,-    list,-  )-import Data.ByteString.Base16 (decodeBase16, parseBase16)-import Data.ByteString.Builder (lazyByteString)-import Data.ByteString.Char8 qualified as C-import Data.ByteString.Lazy qualified as L-import Data.Bytes.Get-import Data.Bytes.Put-import Data.Bytes.Serial-import Data.Char (isSpace)-import Data.Default (Default (..))-import Data.Function ((&))-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as HashMap-import Data.HashSet (HashSet)-import Data.HashSet qualified as HashSet-import Data.Int (Int64)-import Data.List (nub)-import Data.Maybe-  ( catMaybes,-    fromJust,-    fromMaybe,-    isJust,-    mapMaybe,-    maybeToList,-  )-import Data.Proxy (Proxy (..))-import Data.Serialize (decode)-import Data.String (fromString)-import Data.String.Conversions (cs)-import Data.Text (Text)-import Data.Text qualified as T-import Data.Text.Encoding qualified as T-import Data.Text.Lazy qualified as TL-import Data.Time.Clock.POSIX (getPOSIXTime)-import Data.Word (Word32, Word64)-import Database.RocksDB-  ( Property (..),-    getProperty,-  )-import Haskoin.Address-import Haskoin.Block qualified as H-import Haskoin.Crypto.Keys-import Haskoin.Network-import Haskoin.Node-  ( Chain,-    Logger,-    OnlinePeer (..),-    PeerMgr,-    chainGetAncestor,-    chainGetBest,-    getPeers,-    logDebug,-    logError,-    sendMessage,-    withSource,-  )-import Haskoin.Script-import Haskoin.Store.BlockStore-import Haskoin.Store.Cache-import Haskoin.Store.Common-import Haskoin.Store.Data-import Haskoin.Store.Database.Reader-import Haskoin.Store.Manager-import Haskoin.Store.WebCommon-import Haskoin.Transaction-import Haskoin.Util-import NQE-  ( Publisher,-    mailboxFullSTM,-    newBoundedInbox,-    receiveSTM,-    sendSTM,-    subscribe,-    unsubscribe,-  )-import Network.HTTP.Types-  ( Status (..),-    status400,-    status404,-    status409,-    status413,-    status500,-    status503,-    statusIsClientError,-    statusIsServerError,-    statusIsSuccessful,-  )-import Network.Wai-  ( Middleware,-    Request (..),-    responseStatus,-  )-import Network.Wai.Handler.Warp-  ( defaultSettings,-    setHost,-    setPort,-  )-import Network.Wai.Handler.WebSockets (websocketsOr)-import Network.WebSockets-  ( acceptRequest,-    defaultConnectionOptions,-    pendingRequest,-    rejectRequestWith,-    requestPath,-    sendTextData,-  )-import Network.WebSockets qualified as WebSockets-import Network.WebSockets.Connection.PingPong-  ( PingPongOptions (..),-    defaultPingPongOptions,-    withPingPong,-  )-import Network.Wreq qualified as Wreq-import Network.Wreq.Session as Wreq (Session)-import Network.Wreq.Session qualified as Wreq.Session-import Numeric.Natural (Natural)-import System.Metrics.StatsD-import UnliftIO-  ( MonadIO,-    MonadUnliftIO,-    TVar,-    askRunInIO,-    async,-    atomically,-    bracket,-    bracket_,-    checkSTM,-    handleAny,-    liftIO,-    newTVarIO,-    orElse,-    race_,-    readTVar,-    readTVarIO,-    withAsync,-    writeTVar,-  )-import UnliftIO.Concurrent (threadDelay)-import Web.Scotty.Trans qualified as S---- | Scotty application with access to the web state.-type ScottyT m = S.ScottyT (ReaderT WebState m)---- | Scotty request handler with access to the web state.-type ActionT m = S.ActionT (ReaderT WebState m)---- | Limits of the API. A value of @0@ means no limit.-data WebLimits = WebLimits-  { -- | maximum number of items in a list response-    maxItemCount :: !Word32,-    -- | maximum number of full transactions in a list response-    maxFullItemCount :: !Word32,-    -- | maximum offset of a paged query-    maxOffset :: !Word32,-    -- | number of items in a list response when no limit is given-    defItemCount :: !Word32,-    -- | consecutive unused addresses after which xpub derivation stops-    xpubGap :: !Word32,-    -- | unused addresses to look at for an xpub with no transactions-    xpubGapInit :: !Word32,-    -- | seconds without a new transaction before the store is reported-    -- unhealthy-    txTimeout :: !Word64,-    -- | seconds without a new block before the store is reported unhealthy-    blockTimeout :: !Word64-  }-  deriving (Eq, Show)---- | 200000 items, 5000 full transactions, offset up to 50000, 100 items by--- default, xpub gaps of 32 and 20, transaction timeout of 30 minutes and--- block timeout of four hours.-instance Default WebLimits where-  def =-    WebLimits-      { maxItemCount = 200000,-        maxFullItemCount = 5000,-        maxOffset = 50000,-        defItemCount = 100,-        xpubGap = 32,-        xpubGapInit = 20,-        txTimeout = 3600 `div` 2,-        blockTimeout = 4 * 3600-      }---- | Configuration of the web server.-data WebConfig = WebConfig-  { -- | address to listen on, such as @*@ for all-    host :: !String,-    -- | port to listen on-    port :: !Int,-    -- | running store to serve-    store :: !Store,-    -- | blocks that the database may lag behind the headers before the store-    -- is reported unhealthy-    maxLaggingBlocks :: !Int,-    -- | transactions that may be waiting to be imported before the store is-    -- reported unhealthy-    maxPendingTxs :: !Int,-    -- | fewest peers before the store is reported unhealthy-    minPeers :: !Int,-    -- | limits of the API-    limits :: !WebLimits,-    -- | version reported by the health check-    version :: !String,-    -- | the mempool is not indexed, so do not report it in health checks-    noMempool :: !Bool,-    -- | StatsD client, if metrics are enabled-    stats :: !(Maybe Stats),-    -- | microseconds between updates of the price ticker-    tickerRefresh :: !Int,-    -- | URL of the price ticker service-    tickerURL :: !String,-    -- | URL of the historical price service-    priceHistoryURL :: !String,-    -- | disable the xpub endpoints-    noXPub :: !Bool,-    -- | disable the Blockchain.info compatible endpoints-    noBlockchainInfo :: !Bool,-    -- | seconds between health checks-    healthCheckInterval :: !Int,-    -- | log messages here-    logger :: !Logger-  }---- | State shared by all request handlers.-data WebState = WebState-  { -- | configuration-    config :: !WebConfig,-    -- | latest prices by currency code-    ticker :: !(TVar (HashMap Text BinfoTicker)),-    -- | StatsD metrics, if enabled-    metrics :: !(Maybe WebMetrics),-    -- | HTTP session for price requests-    session :: !Wreq.Session,-    -- | result of the latest health check-    health :: !(TVar HealthCheck)-  }---- | StatsD metrics of the web server: a timing for each kind of request,--- under @web.\<name\>@, error counters and a gauge of connected event--- clients.-data WebMetrics = WebMetrics-  { -- Addresses-    addressTx :: !StatTiming,-    addressTxFull :: !StatTiming,-    addressBalance :: !StatTiming,-    addressUnspent :: !StatTiming,-    xpub :: !StatTiming,-    xpubDelete :: !StatTiming,-    xpubTxFull :: !StatTiming,-    xpubTx :: !StatTiming,-    xpubBalance :: !StatTiming,-    xpubUnspent :: !StatTiming,-    -- Transactions-    tx :: !StatTiming,-    txRaw :: !StatTiming,-    txAfter :: !StatTiming,-    txBlock :: !StatTiming,-    txBlockRaw :: !StatTiming,-    txPost :: !StatTiming,-    mempool :: !StatTiming,-    -- Blocks-    block :: !StatTiming,-    blockRaw :: !StatTiming,-    -- Blockchain-    binfoMultiaddr :: !StatTiming,-    binfoBalance :: !StatTiming,-    binfoAddressRaw :: !StatTiming,-    binfoUnspent :: !StatTiming,-    binfoTxRaw :: !StatTiming,-    binfoBlock :: !StatTiming,-    binfoBlockHeight :: !StatTiming,-    binfoBlockLatest :: !StatTiming,-    binfoBlockRaw :: !StatTiming,-    binfoMempool :: !StatTiming,-    binfoExportHistory :: !StatTiming,-    -- Blockchain /q endpoints-    binfoQaddresstohash :: !StatTiming,-    binfoQhashtoaddress :: !StatTiming,-    binfoQaddrpubkey :: !StatTiming,-    binfoQpubkeyaddr :: !StatTiming,-    binfoQhashpubkey :: !StatTiming,-    binfoQgetblockcount :: !StatTiming,-    binfoQlatesthash :: !StatTiming,-    binfoQbcperblock :: !StatTiming,-    binfoQtxtotalbtcoutput :: !StatTiming,-    binfoQtxtotalbtcinput :: !StatTiming,-    binfoQtxfee :: !StatTiming,-    binfoQtxresult :: !StatTiming,-    binfoQgetreceivedbyaddress :: !StatTiming,-    binfoQgetsentbyaddress :: !StatTiming,-    binfoQaddressbalance :: !StatTiming,-    binfoQaddressfirstseen :: !StatTiming,-    -- Errors-    serverErrors :: StatCounter,-    clientErrors :: StatCounter,-    -- Others-    health :: !StatTiming,-    peers :: !StatTiming,-    db :: !StatTiming,-    events :: !StatGauge-  }---- | Register the web server metrics with a StatsD client.-createMetrics :: (MonadIO m) => Stats -> m WebMetrics-createMetrics s = liftIO $ do-  -- Addresses-  addressTx <- d "address_transactions"-  addressTxFull <- d "address_transactions_full"-  addressBalance <- d "address_balance"-  addressUnspent <- d "address_unspent"-  xpub <- d "xpub"-  xpubDelete <- d "xpub_delete"-  xpubTxFull <- d "xpub_transactions_full"-  xpubTx <- d "xpub_transactions"-  xpubBalance <- d "xpub_balances"-  xpubUnspent <- d "xpub_unspent"--  -- Transactions-  tx <- d "transaction"-  txRaw <- d "transaction_raw"-  txAfter <- d "transaction_after"-  txPost <- d "transaction_post"-  txBlock <- d "transactions_block"-  txBlockRaw <- d "transactions_block_raw"-  mempool <- d "mempool"--  -- Blocks-  block <- d "block"-  blockRaw <- d "block_raw"--  -- Blockchain-  binfoMultiaddr <- d "blockchain_multiaddr"-  binfoBalance <- d "blockchain_balance"-  binfoAddressRaw <- d "blockchain_rawaddr"-  binfoUnspent <- d "blockchain_unspent"-  binfoTxRaw <- d "blockchain_rawtx"-  binfoBlock <- d "blockchain_blocks"-  binfoBlockHeight <- d "blockchain_block_height"-  binfoBlockLatest <- d "blockchain_latestblock"-  binfoBlockRaw <- d "blockchain_rawblock"-  binfoMempool <- d "blockchain_mempool"-  binfoExportHistory <- d "blockchain_export_history"--  -- Blockchain /q endpoints-  binfoQaddresstohash <- d "blockchain_q_addresstohash"-  binfoQhashtoaddress <- d "blockchain_q_hashtoaddress"-  binfoQaddrpubkey <- d "blockckhain_q_addrpubkey"-  binfoQpubkeyaddr <- d "blockchain_q_pubkeyaddr"-  binfoQhashpubkey <- d "blockchain_q_hashpubkey"-  binfoQgetblockcount <- d "blockchain_q_getblockcount"-  binfoQlatesthash <- d "blockchain_q_latesthash"-  binfoQbcperblock <- d "blockchain_q_bcperblock"-  binfoQtxtotalbtcoutput <- d "blockchain_q_txtotalbtcoutput"-  binfoQtxtotalbtcinput <- d "blockchain_q_txtotalbtcinput"-  binfoQtxfee <- d "blockchain_q_txfee"-  binfoQtxresult <- d "blockchain_q_txresult"-  binfoQgetreceivedbyaddress <- d "blockchain_q_getreceivedbyaddress"-  binfoQgetsentbyaddress <- d "blockchain_q_getsentbyaddress"-  binfoQaddressbalance <- d "blockchain_q_addressbalance"-  binfoQaddressfirstseen <- d "blockchain_q_addressfirstseen"--  -- Errors-  serverErrors <- c "server_errors"-  clientErrors <- c "client_errors"--  -- Others-  health <- d "health"-  peers <- d "peers"-  db <- d "dbstats"--  events <- g "events_connected"-  return WebMetrics {..}-  where-    d x = newStatTiming s ("web." <> x) 10-    g x = newStatGauge s ("web." <> x) 0-    c x = newStatCounter s ("web." <> x) 10---- | Increase a gauge while an action runs.-withGaugeIO :: (MonadUnliftIO m) => StatGauge -> m a -> m a-withGaugeIO g = bracket_ (incrementGauge g 1) (decrementGauge g 1)---- | Increase a gauge of the web metrics while a handler runs.-withGaugeIncrease ::-  (MonadUnliftIO m) =>-  (WebMetrics -> StatGauge) ->-  ActionT m a ->-  ActionT m a-withGaugeIncrease gf go =-  askl (.metrics) >>= \case-    Nothing -> go-    Just m -> do-      s <- liftWith $ \run -> withGaugeIO (gf m) (run go)-      restoreT $ return s---- | Record how long a handler takes in a timing of the web metrics.-withMetrics ::-  (MonadUnliftIO m) => (WebMetrics -> StatTiming) -> ActionT m a -> ActionT m a-withMetrics df go =-  askl (.metrics) >>= \case-    Nothing -> go-    Just m ->-      restoreT . return-        =<< liftWith (\run -> bracket time (stop m) (const (run go)))-  where-    time = round . (* 1000) <$> liftIO getPOSIXTime-    stop m t1 = do-      t2 <- time-      let ms = t2 - t1-      liftIO $ addTiming (df m) ms---- | Encoding of a response.-data SerialAs-  = -- | binary, for @Accept: application\/octet-stream@-    SerialAsBinary-  | -- | compact JSON-    SerialAsJSON-  | -- | indented JSON, for @?pretty=true@-    SerialAsPrettyJSON-  deriving (Eq, Show)--instance-  (MonadUnliftIO m) =>-  StoreReadBase (ReaderT WebState m)-  where-  getCtx = runInWebReader getCtx-  getNetwork = runInWebReader getNetwork-  getBestBlock = runInWebReader getBestBlock-  getBlocksAtHeight height = runInWebReader (getBlocksAtHeight height)-  getBlock bh = runInWebReader (getBlock bh)-  getTxData th = runInWebReader (getTxData th)-  getManyTxData ths = runInWebReader (getManyTxData ths)-  getSpender op = runInWebReader (getSpender op)-  getUnspent op = runInWebReader (getUnspent op)-  getBalance a = runInWebReader (getBalance a)-  getMempool = runInWebReader getMempool-  getMempoolPage = runInWebReader . getMempoolPage--instance-  (MonadUnliftIO m) =>-  StoreReadExtra (ReaderT WebState m)-  where-  getMaxGap = runInWebReader getMaxGap-  getInitialGap = runInWebReader getInitialGap-  getBalances as = runInWebReader (getBalances as)-  getAddressesTxs as = runInWebReader . getAddressesTxs as-  getAddressTxs a = runInWebReader . getAddressTxs a-  getAddressUnspents a = runInWebReader . getAddressUnspents a-  getAddressesUnspents as = runInWebReader . getAddressesUnspents as-  xPubBals = runInWebReader . xPubBals-  xPubUnspents xpub xbals = runInWebReader . xPubUnspents xpub xbals-  xPubTxs xpub xbals = runInWebReader . xPubTxs xpub xbals-  xPubTxCount xpub = runInWebReader . xPubTxCount xpub-  getNumTxData = runInWebReader . getNumTxData--instance (MonadUnliftIO m) => StoreReadBase (ActionT m) where-  getCtx = lift getCtx-  getNetwork = lift getNetwork-  getBestBlock = lift getBestBlock-  getBlocksAtHeight = lift . getBlocksAtHeight-  getBlock = lift . getBlock-  getTxData = lift . getTxData-  getManyTxData = lift . getManyTxData-  getSpender = lift . getSpender-  getUnspent = lift . getUnspent-  getBalance = lift . getBalance-  getMempool = lift getMempool-  getMempoolPage = lift . getMempoolPage--instance (MonadUnliftIO m) => StoreReadExtra (ActionT m) where-  getBalances = lift . getBalances-  getAddressesTxs as = lift . getAddressesTxs as-  getAddressTxs a = lift . getAddressTxs a-  getAddressUnspents a = lift . getAddressUnspents a-  getAddressesUnspents as = lift . getAddressesUnspents as-  xPubBals = lift . xPubBals-  xPubUnspents xpub xbals = lift . xPubUnspents xpub xbals-  xPubTxs xpub xbals = lift . xPubTxs xpub xbals-  xPubTxCount xpub = lift . xPubTxCount xpub-  getMaxGap = lift getMaxGap-  getInitialGap = lift getInitialGap-  getNumTxData = lift . getNumTxData------------------------ Path Handlers -------------------------- | Run the web server forever. Also starts a thread that updates prices, unless--- the Blockchain.info endpoints are disabled, and a thread that runs health--- checks.-runWeb :: (MonadUnliftIO m) => WebConfig -> m ()-runWeb config = do-  ticker <- newTVarIO HashMap.empty-  metrics <- mapM createMetrics config.stats-  session <- liftIO Wreq.Session.newAPISession-  health <- runHealthCheck >>= newTVarIO-  let state = WebState {..}-  withAsync (priceUpdater session ticker) $ \_a1 ->-    withAsync (healthCheckLoop health) $ \_a2 -> do-      runner <- askRunInIO-      S.scottyOptsT opts (runner . flip runReaderT state) $ do-        S.middleware $ webSocketEvents state-        S.middleware (logIt config.logger)-        handlePaths config-        S.notFound $ raise ThingNotFound-  where-    priceUpdater session =-      unless config.noBlockchainInfo-        . price-          (withSource "Price" config.logger)-          config.store.net-          session-          config.tickerURL-          config.tickerRefresh-    runHealthCheck = runReaderT (healthCheck config) config.store.db-    healthCheckLoop v = loopForever $ do-      threadDelay (config.healthCheckInterval * 1000 * 1000)-      runHealthCheck >>= atomically . writeTVar v-    opts = S.defaultOptions {S.settings = settings defaultSettings}-    settings = setPort config.port . setHost (fromString config.host)---- | Historical prices of the coin of the network in a currency at several times. Returns an empty list on failure.-getRates ::-  (MonadUnliftIO m) =>-  Logger ->-  Network ->-  Wreq.Session ->-  String ->-  Text ->-  [Word64] ->-  m [BinfoRate]-getRates logger net session url currency times = do-  handleAny err $ do-    r <--      liftIO $-        Wreq.asJSON-          =<< Wreq.Session.postWith opts session url body-    return $ r ^. Wreq.responseBody-  where-    err _ = do-      logError logger "Could not get historic prices"-      return []-    body = toJSON times-    base =-      Wreq.defaults-        & Wreq.param "base" .~ [T.toUpper $ T.pack net.name]-    opts = base & Wreq.param "quote" .~ [currency]---- | Update the prices of the coin of the network periodically, for BTC and BCH only.-price ::-  (MonadUnliftIO m) =>-  Logger ->-  Network ->-  Wreq.Session ->-  String ->-  Int ->-  TVar (HashMap Text BinfoTicker) ->-  m ()-price logger net session url pget v = forM_ purl $ \u -> loopForever $ do-  let err e = logError logger $ cs (show e)-  handleAny err $ do-    r <- liftIO $ Wreq.asJSON =<< Wreq.Session.get session u-    atomically . writeTVar v $ r ^. Wreq.responseBody-  threadDelay pget-  where-    purl = case code of-      Nothing -> Nothing-      Just x -> Just (url <> "?base=" <> x)-      where-        code-          | net == btc = Just "btc"-          | net == bch = Just "bch"-          | otherwise = Nothing---- | Respond with an error and stop handling the request.-raise :: (MonadIO m) => Except -> ActionT m a-raise e = do-  askl (.metrics) >>= mapM_ \m -> do-    liftIO $-      mapM_ (`incrementCounter` 1) $-        if-          | statusIsClientError (errStatus e) -> Just m.clientErrors-          | statusIsServerError (errStatus e) -> Just m.serverErrors-          | otherwise -> Nothing-  setHeaders-  S.status $ errStatus e-  S.json e-  S.finish---- | HTTP status of an error.-errStatus :: Except -> Status-errStatus ThingNotFound = status404-errStatus BadRequest = status400-errStatus UserError {} = status400-errStatus StringError {} = status400-errStatus ServerError = status500-errStatus TxIndexConflict {} = status409-errStatus ServerTimeout = status500-errStatus RequestTooLarge = status413---- | Register the handlers of every endpoint, except those disabled in the configuration.-handlePaths ::-  (MonadUnliftIO m) =>-  WebConfig ->-  S.ScottyT (ReaderT WebState m) ()-handlePaths cfg = do-  -- Block Paths-  pathCompact-    (GetBlockBest <$> paramDef)-    scottyBlockBest-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlockHeight <$> paramCapture <*> paramDef)-    (fmap SerialList . scottyBlockHeight)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetBlockTime <$> paramCapture <*> paramDef)-    scottyBlockTime-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlockMTP <$> paramCapture <*> paramDef)-    scottyBlockMTP-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlock <$> paramCapture <*> paramDef)-    scottyBlock-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetTx <$> paramCapture)-    scottyTx-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetTxRaw <$> paramCapture)-    scottyTxRaw-    toEncoding-    toJSON-  pathCompact-    (PostTx <$> parseTxBody)-    scottyPostTx-    toEncoding-    toJSON-  pathCompact-    (GetAddrTxs <$> paramCapture <*> parseLimits)-    (fmap SerialList . scottyAddrTxs)-    toEncoding-    toJSON-  pathCompact-    (GetAddrBalance <$> paramCapture)-    scottyAddrBalance-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetAddrUnspent <$> paramCapture <*> parseLimits)-    (fmap SerialList . scottyAddrUnspent)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetPeers & return)-    (fmap SerialList . scottyPeers)-    toEncoding-    toJSON-  pathCompact-    (GetHealth & return)-    scottyHealth-    toEncoding-    toJSON-  pathCompact-    (GetMempool <$> paramOptional <*> parseOffset)-    (fmap SerialList . scottyMempool)-    toEncoding-    toJSON-  pathCompact-    (GetBlocks <$> paramRequired <*> paramDef)-    (fmap SerialList . scottyBlocks)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetBlockRaw <$> paramCapture)-    scottyBlockRaw-    toEncoding-    toJSON-  pathCompact-    (GetBlockBestRaw & return)-    scottyBlockBestRaw-    toEncoding-    toJSON-  pathCompact-    (GetBlockLatest <$> paramDef)-    (fmap SerialList . scottyBlockLatest)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetBlockHeights <$> paramRequired <*> paramDef)-    (fmap SerialList . scottyBlockHeights)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetBlockHeightRaw <$> paramCapture)-    scottyBlockHeightRaw-    toEncoding-    toJSON-  pathCompact-    (GetBlockTimeRaw <$> paramCapture)-    scottyBlockTimeRaw-    toEncoding-    toJSON-  pathCompact-    (GetBlockMTPRaw <$> paramCapture)-    scottyBlockMTPRaw-    toEncoding-    toJSON-  pathCompact-    (GetTxs <$> paramRequired)-    (fmap SerialList . scottyTxs)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetTxsRaw <$> paramRequired)-    scottyTxsRaw-    toEncoding-    toJSON-  pathCompact-    (GetTxsBlock <$> paramCapture)-    (fmap SerialList . scottyTxsBlock)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetTxsBlockRaw <$> paramCapture)-    scottyTxsBlockRaw-    toEncoding-    toJSON-  pathCompact-    (GetTxAfter <$> paramCapture <*> paramCapture)-    scottyTxAfter-    toEncoding-    toJSON-  pathCompact-    (GetAddrsTxs <$> paramRequired <*> parseLimits)-    (fmap SerialList . scottyAddrsTxs)-    toEncoding-    toJSON-  pathCompact-    (GetAddrTxsFull <$> paramCapture <*> parseLimits)-    (fmap SerialList . scottyAddrTxsFull)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetAddrsTxsFull <$> paramRequired <*> parseLimits)-    (fmap SerialList . scottyAddrsTxsFull)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetAddrsBalance <$> paramRequired)-    (fmap SerialList . scottyAddrsBalance)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetAddrsUnspent <$> paramRequired <*> parseLimits)-    (fmap SerialList . scottyAddrsUnspent)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  S.get "/events" scottyEvents-  S.get "/dbstats" scottyDbStats-  unless cfg.noXPub $ do-    pathCompact-      (GetXPub <$> paramCapture <*> paramDef <*> paramDef)-      scottyXPub-      toEncoding-      toJSON-    pathCompact-      (GetXPubTxs <$> paramCapture <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxs)-      toEncoding-      toJSON-    pathCompact-      (GetXPubTxsFull <$> paramCapture <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxsFull)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetXPubBalances <$> paramCapture <*> paramDef <*> paramDef)-      (fmap SerialList . scottyXPubBalances)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetXPubUnspent <$> paramCapture <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubUnspent)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (DelCachedXPub <$> paramCapture <*> paramDef)-      scottyDelXPub-      toEncoding-      toJSON-  unless cfg.noBlockchainInfo $ do-    S.post "/blockchain/multiaddr" scottyMultiAddr-    S.get "/blockchain/multiaddr" scottyMultiAddr-    S.get "/blockchain/balance" scottyShortBal-    S.post "/blockchain/balance" scottyShortBal-    S.get "/blockchain/rawaddr/:addr" scottyRawAddr-    S.get "/blockchain/address/:addr" scottyRawAddr-    S.get "/blockchain/xpub/:addr" scottyRawAddr-    S.post "/blockchain/unspent" scottyBinfoUnspent-    S.get "/blockchain/unspent" scottyBinfoUnspent-    S.get "/blockchain/rawtx/:txid" scottyBinfoTx-    S.get "/blockchain/rawblock/:block" scottyBinfoBlock-    S.get "/blockchain/latestblock" scottyBinfoLatest-    S.get "/blockchain/unconfirmed-transactions" scottyBinfoMempool-    S.get "/blockchain/block-height/:height" scottyBinfoBlockHeight-    S.get "/blockchain/blocks/:milliseconds" scottyBinfoBlocksDay-    S.get "/blockchain/export-history" scottyBinfoHistory-    S.post "/blockchain/export-history" scottyBinfoHistory-    S.get "/blockchain/q/addresstohash/:addr" scottyBinfoAddrToHash-    S.get "/blockchain/q/hashtoaddress/:hash" scottyBinfoHashToAddr-    S.get "/blockchain/q/addrpubkey/:pubkey" scottyBinfoAddrPubkey-    S.get "/blockchain/q/pubkeyaddr/:addr" scottyBinfoPubKeyAddr-    S.get "/blockchain/q/hashpubkey/:pubkey" scottyBinfoHashPubkey-    S.get "/blockchain/q/getblockcount" scottyBinfoGetBlockCount-    S.get "/blockchain/q/latesthash" scottyBinfoLatestHash-    S.get "/blockchain/q/bcperblock" scottyBinfoSubsidy-    S.get "/blockchain/q/txtotalbtcoutput/:txid" scottyBinfoTotalOut-    S.get "/blockchain/q/txtotalbtcinput/:txid" scottyBinfoTotalInput-    S.get "/blockchain/q/txfee/:txid" scottyBinfoTxFees-    S.get "/blockchain/q/txresult/:txid/:addr" scottyBinfoTxResult-    S.get "/blockchain/q/getreceivedbyaddress/:addr" scottyBinfoReceived-    S.get "/blockchain/q/getsentbyaddress/:addr" scottyBinfoSent-    S.get "/blockchain/q/addressbalance/:addr" scottyBinfoAddrBalance-    S.get "/blockchain/q/addressfirstseen/:addr" scottyFirstSeen-  where-    json_list f = toJSONList . map f-    net = cfg.store.net---- | Register a handler for an API resource, with compact JSON by default.-pathCompact ::-  (ApiResource a b, MonadUnliftIO m) =>-  ActionT m a ->-  (a -> ActionT m b) ->-  (b -> Encoding) ->-  (b -> Value) ->-  S.ScottyT (ReaderT WebState m) ()-pathCompact parser action encJson encValue =-  pathCommon parser action encJson encValue False---- | Register a handler for an API resource: parse the request, run the handler,--- and encode the response as JSON or binary, as the client asked.-pathCommon ::-  (ApiResource a b, MonadUnliftIO m) =>-  ActionT m a ->-  (a -> ActionT m b) ->-  (b -> Encoding) ->-  (b -> Value) ->-  Bool ->-  ScottyT m ()-pathCommon parser action encJson encValue pretty =-  S.addroute (resourceMethod proxy) (capturePath proxy) $ do-    setHeaders-    proto <- setupContentType pretty-    apiRes <- parser-    res <- action apiRes-    S.raw $ protoSerial proto encJson encValue res-  where-    toProxy :: ActionT m a -> Proxy a-    toProxy = const Proxy-    proxy = toProxy parser---- | Respond with JSON.-streamEncoding :: (MonadIO m) => Encoding -> ActionT m ()-streamEncoding e = do-  S.setHeader "Content-Type" "application/json; charset=utf-8"-  S.raw (encodingToLazyByteString e)---- | Encode a response in binary, JSON or pretty JSON.-protoSerial ::-  (Serial a) =>-  SerialAs ->-  (a -> Encoding) ->-  (a -> Value) ->-  a ->-  L.ByteString-protoSerial SerialAsBinary _ _ = runPutL . serialize-protoSerial SerialAsJSON f _ = encodingToLazyByteString . f-protoSerial SerialAsPrettyJSON _ g =-  encodePretty' defConfig {confTrailingNewline = True} . g---- | Allow requests from any origin.-setHeaders :: (MonadIO m) => S.ActionT m ()-setHeaders = S.setHeader "Access-Control-Allow-Origin" "*"---- | Respond with JSON, pretty if the @pretty@ parameter or the default says so.-setupJSON :: (MonadUnliftIO m) => Bool -> S.ActionT m SerialAs-setupJSON pretty = do-  S.setHeader "Content-Type" "application/json"-  p <- param "pretty" `rescue` return pretty-  return $ if p then SerialAsPrettyJSON else SerialAsJSON---- | Respond with binary.-setupBinary :: (MonadIO m) => S.ActionT m SerialAs-setupBinary = do-  S.setHeader "Content-Type" "application/octet-stream"-  return SerialAsBinary---- | Choose the encoding of the response from the @Accept@ header of the request.-setupContentType :: (MonadUnliftIO m) => Bool -> S.ActionT m SerialAs-setupContentType pretty = do-  accept <- S.header "accept"-  maybe (setupJSON pretty) setType accept-  where-    setType "application/octet-stream" = setupBinary-    setType _ = setupJSON pretty---- GET Block / GET Blocks ------ | Handler for @GET \/block\/:block@: a block by hash.-scottyBlock ::-  (MonadUnliftIO m) => GetBlock -> ActionT m BlockData-scottyBlock (GetBlock h (NoTx noTx)) = withMetrics (.block) $ do-  getBlock h >>= \case-    Nothing ->-      raise ThingNotFound-    Just b -> do-      return $ pruneTx noTx b---- | Blocks by hash, without duplicates, leaving out unknown ones.-getBlocks ::-  (MonadUnliftIO m) =>-  [H.BlockHash] ->-  Bool ->-  ActionT m [BlockData]-getBlocks hs notx =-  (pruneTx notx <$>) . catMaybes <$> mapM getBlock (nub hs)---- | Handler for @GET \/blocks?blocks=…@: several blocks by hash.-scottyBlocks ::-  (MonadUnliftIO m) => GetBlocks -> ActionT m [BlockData]-scottyBlocks (GetBlocks hs (NoTx notx)) =-  withMetrics (.block) $-    getBlocks hs notx---- | Keep only the coinbase in the list of transactions of a block, if the flag is 'True'.-pruneTx :: Bool -> BlockData -> BlockData-pruneTx False b = b-pruneTx True BlockData {..} = BlockData {txs = take 1 txs, ..}---- GET BlockRaw ------ | Handler for @GET \/block\/:block\/raw@: a block by hash, serialized as in the peer-to-peer protocol.-scottyBlockRaw ::-  (MonadUnliftIO m) =>-  GetBlockRaw ->-  ActionT m (RawResult H.Block)-scottyBlockRaw (GetBlockRaw h) =-  withMetrics (.blockRaw) $-    RawResult <$> getRawBlock h---- | Block by hash with all its transactions. Responds with an error if not found.-getRawBlock ::-  (MonadUnliftIO m) =>-  H.BlockHash ->-  ActionT m H.Block-getRawBlock h =-  getBlock h >>= maybe (raise ThingNotFound) return >>= lift . toRawBlock---- | Rebuild a block from its data and its transactions.-toRawBlock :: (StoreReadBase m) => BlockData -> m H.Block-toRawBlock b = do-  txs <- map transactionData . catMaybes <$> getTransactions b.txs-  return H.Block {header = b.header, txs}---- GET BlockBest / BlockBestRaw ------ | Handler for @GET \/block\/best@: the best block.-scottyBlockBest ::-  (MonadUnliftIO m) => GetBlockBest -> ActionT m BlockData-scottyBlockBest (GetBlockBest (NoTx notx)) =-  withMetrics (.block) $-    maybe (raise ThingNotFound) (return . pruneTx notx) <=< runMaybeT $-      MaybeT getBestBlock >>= MaybeT . getBlock---- | Handler for @GET \/block\/best\/raw@: the best block, serialized.-scottyBlockBestRaw ::-  (MonadUnliftIO m) =>-  GetBlockBestRaw ->-  ActionT m (RawResult H.Block)-scottyBlockBestRaw _ =-  withMetrics (.blockRaw) $-    maybe (raise ThingNotFound) (return . RawResult) <=< runMaybeT $-      MaybeT getBestBlock >>= lift . getRawBlock---- GET BlockLatest ------ | Handler for @GET \/block\/latest@: the best block and its ancestors, up to 100 blocks.-scottyBlockLatest ::-  (MonadUnliftIO m) =>-  GetBlockLatest ->-  ActionT m [BlockData]-scottyBlockLatest (GetBlockLatest (NoTx noTx)) =-  withMetrics (.block) $-    getBestBlock >>= maybe (raise ThingNotFound) (go [] <=< getBlock)-  where-    go acc Nothing = return $ reverse acc-    go acc (Just b)-      | b.height <= 0 = return $ reverse acc-      | length acc == 99 = return . reverse $ pruneTx noTx b : acc-      | otherwise = go (pruneTx noTx b : acc) =<< getBlock b.header.prev---- GET BlockHeight / BlockHeights / BlockHeightRaw ------ | Handler for @GET \/block\/height\/:height@: the blocks at a height.-scottyBlockHeight ::-  (MonadUnliftIO m) => GetBlockHeight -> ActionT m [BlockData]-scottyBlockHeight (GetBlockHeight h (NoTx notx)) =-  withMetrics (.block) $-    getBlocksAtHeight (fromIntegral h)-      >>= (`getBlocks` notx)---- | Handler for @GET \/block\/heights?heights=…@: the blocks at several heights.-scottyBlockHeights ::-  (MonadUnliftIO m) =>-  GetBlockHeights ->-  ActionT m [BlockData]-scottyBlockHeights (GetBlockHeights (HeightsParam heights) (NoTx notx)) =-  withMetrics (.block) $-    mapM (getBlocksAtHeight . fromIntegral) heights-      >>= (`getBlocks` notx) . concat---- | Handler for @GET \/block\/height\/:height\/raw@: the blocks at a height, serialized.-scottyBlockHeightRaw ::-  (MonadUnliftIO m) =>-  GetBlockHeightRaw ->-  ActionT m (RawResultList H.Block)-scottyBlockHeightRaw (GetBlockHeightRaw h) =-  withMetrics (.blockRaw) $-    fmap RawResultList $-      mapM getRawBlock =<< getBlocksAtHeight (fromIntegral h)---- GET BlockTime / BlockTimeRaw ------ | Handler for @GET \/block\/time\/:time@: the last block with a timestamp at or before a time.-scottyBlockTime ::-  (MonadUnliftIO m) =>-  GetBlockTime ->-  ActionT m BlockData-scottyBlockTime (GetBlockTime (TimeParam t) (NoTx notx)) =-  withMetrics (.block) $ do-    ch <- askl (.config.store.chain)-    blockAtOrBefore ch t >>= \case-      Nothing -> raise ThingNotFound-      Just b -> return $ pruneTx notx b---- | Handler for @GET \/block\/mtp\/:time@: the first block with a median time past at or after a time.-scottyBlockMTP ::-  (MonadUnliftIO m) =>-  GetBlockMTP ->-  ActionT m BlockData-scottyBlockMTP (GetBlockMTP (TimeParam t) (NoTx notx)) =-  withMetrics (.block) $ do-    ch <- askl (.config.store.chain)-    blockAtOrAfterMTP ch t >>= \case-      Nothing -> raise ThingNotFound-      Just b -> return $ pruneTx notx b---- | Handler for @GET \/block\/time\/:time\/raw@: the last block with a timestamp at or before a time, serialized.-scottyBlockTimeRaw ::-  (MonadUnliftIO m) =>-  GetBlockTimeRaw ->-  ActionT m (RawResult H.Block)-scottyBlockTimeRaw (GetBlockTimeRaw (TimeParam t)) =-  withMetrics (.blockRaw) $ do-    ch <- askl (.config.store.chain)-    blockAtOrBefore ch t >>= \case-      Nothing -> raise ThingNotFound-      Just b -> RawResult <$> lift (toRawBlock b)---- | Handler for @GET \/block\/mtp\/:time\/raw@: the first block with a median time past at or after a time, serialized.-scottyBlockMTPRaw ::-  (MonadUnliftIO m) =>-  GetBlockMTPRaw ->-  ActionT m (RawResult H.Block)-scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) =-  withMetrics (.blockRaw) $ do-    ch <- askl (.config.store.chain)-    blockAtOrAfterMTP ch t >>= \case-      Nothing -> raise ThingNotFound-      Just b -> RawResult <$> lift (toRawBlock b)---- GET Transactions ------ | Handler for @GET \/transaction\/:txid@: a transaction.-scottyTx :: (MonadUnliftIO m) => GetTx -> ActionT m Transaction-scottyTx (GetTx txid) =-  withMetrics (.tx) $-    getTransaction txid >>= maybe (raise ThingNotFound) return---- | Handler for @GET \/transactions?txids=…@: several transactions.-scottyTxs ::-  (MonadUnliftIO m) => GetTxs -> ActionT m [Transaction]-scottyTxs (GetTxs txids) =-  withMetrics (.tx) $-    catMaybes <$> getTransactions (nub txids)---- | Handler for @GET \/transaction\/:txid\/raw@: a transaction, serialized.-scottyTxRaw ::-  (MonadUnliftIO m) => GetTxRaw -> ActionT m (RawResult Tx)-scottyTxRaw (GetTxRaw txid) =-  withMetrics (.txRaw) $-    getTransaction txid-      >>= \case-        Nothing -> raise ThingNotFound-        Just tx -> return $ RawResult (transactionData tx)---- | Handler for @GET \/transactions\/raw?txids=…@: several transactions, serialized.-scottyTxsRaw ::-  (MonadUnliftIO m) =>-  GetTxsRaw ->-  ActionT m (RawResultList Tx)-scottyTxsRaw (GetTxsRaw txids) =-  withMetrics (.txRaw) $-    RawResultList . map transactionData . catMaybes-      <$> getTransactions (nub txids)---- | Transactions of a block. Responds with an error if not found.-getTxsBlock ::-  (MonadUnliftIO m) =>-  H.BlockHash ->-  ActionT m [Transaction]-getTxsBlock h =-  getBlock h >>= \case-    Nothing -> raise ThingNotFound-    Just b -> catMaybes <$> getTransactions b.txs---- | Handler for @GET \/transactions\/block\/:block@: the transactions of a block.-scottyTxsBlock ::-  (MonadUnliftIO m) =>-  GetTxsBlock ->-  ActionT m [Transaction]-scottyTxsBlock (GetTxsBlock h) =-  withMetrics (.txBlock) $ getTxsBlock h---- | Handler for @GET \/transactions\/block\/:block\/raw@: the transactions of a block, serialized.-scottyTxsBlockRaw ::-  (MonadUnliftIO m) =>-  GetTxsBlockRaw ->-  ActionT m (RawResultList Tx)-scottyTxsBlockRaw (GetTxsBlockRaw h) =-  withMetrics (.txBlockRaw) $-    RawResultList . map transactionData <$> getTxsBlock h---- GET TransactionAfterHeight ------ | Handler for @GET \/transaction\/:txid\/after\/:height@: whether any ancestor of a transaction is a coinbase confirmed after a height.-scottyTxAfter ::-  (MonadUnliftIO m) =>-  GetTxAfter ->-  ActionT m (GenericResult (Maybe Bool))-scottyTxAfter (GetTxAfter txid height) =-  withMetrics (.txAfter) $-    GenericResult . fst <$> cbAfterHeight (fromIntegral height) txid---- | Check if any of the ancestors of this transaction is a coinbase after the--- specified height. Returns 'Nothing' if answer cannot be computed before--- hitting limits.-cbAfterHeight ::-  (StoreReadBase m) =>-  H.BlockHeight ->-  TxHash ->-  m (Maybe Bool, Int)-cbAfterHeight height txid =-  inputs n HashSet.empty HashSet.empty [txid]-  where-    n = 10000-    inputs 0 _ _ [] = return (Nothing, 10000)-    inputs i is ns [] =-      let is' = HashSet.union is ns-          ns' = HashSet.empty-          ts = HashSet.toList (HashSet.difference ns is)-       in case ts of-            [] -> return (Just False, n - i)-            _ -> inputs i is' ns' ts-    inputs i is ns (t : ts) =-      getTransaction t >>= \case-        Nothing -> return (Nothing, n - i)-        Just tx-          | height_check tx ->-              if cb_check tx-                then return (Just True, n - i + 1)-                else-                  let ns' = HashSet.union (ins tx) ns-                   in inputs (i - 1) is ns' ts-          | otherwise -> inputs (i - 1) is ns ts-    cb_check = any isCoinbase . (.inputs)-    ins = HashSet.fromList . map (.outpoint.hash) . (.inputs)-    height_check tx =-      case tx.block of-        BlockRef h _ -> h > height-        MemRef _ -> True---- POST Transaction ------ | Handler for @POST \/transactions@: broadcast a transaction to half of the peers and return its hash.-scottyPostTx :: (MonadUnliftIO m) => PostTx -> ActionT m TxId-scottyPostTx (PostTx tx) =-  withMetrics (.txPost) $ do-    cfg <- askl (.config)-    lift (publishTx cfg tx)-    return (TxId (txHash tx))---- | Send transaction to all connected peers.-publishTx ::-  (MonadUnliftIO m, StoreReadBase m) =>-  WebConfig ->-  Tx ->-  m ()-publishTx cfg tx = do-  ps <- liftIO $ getPeers cfg.store.peerMgr-  let c = max 1 (length ps `div` 2)-  forM_ (take c ps) $ \p -> do-    liftIO $ sendMessage (MTx tx) p.mailbox-    void . async $ do-      threadDelay (5 * 1000 * 1000)-      let v = if cfg.store.net.segWit then InvWitnessTx else InvTx-          g = MGetData (GetData [InvVector v (txHash tx).get])-      liftIO $ sendMessage g p.mailbox---- GET Mempool / Events ------ | Handler for @GET \/mempool@: hashes of mempool transactions, newest first.-scottyMempool ::-  (MonadUnliftIO m) => GetMempool -> ActionT m [TxHash]-scottyMempool (GetMempool limitM (OffsetParam o)) =-  withMetrics (.mempool) $ do-    WebLimits {..} <- askl (.config.limits)-    let wl' = WebLimits {maxItemCount = 0, ..}-        l = Limits (validateLimit wl' False limitM) (fromIntegral o) Nothing-    map snd <$> getMempoolPage l---- | Middleware that serves @\/events@ over a WebSocket: each new block or mempool--- transaction is sent as a JSON message. Clients that do not answer pings, or--- that fall more than 10000 events behind, are disconnected.-webSocketEvents :: WebState -> Middleware-webSocketEvents s =-  websocketsOr defaultConnectionOptions (wrap . events)-  where-    pub = s.config.store.pub-    pingPong = defaultPingPongOptions {pingInterval = 30, pongTimeout = 60}-    gauge = (.events) <$> s.metrics-    wrap f = case gauge of-      Nothing -> f-      Just g -> withGaugeIO g f-    events pending = do-      let path = requestPath $ pendingRequest pending-      if path == "/events"-        then withEventSubscription pub $ \next -> do-          conn <- acceptRequest pending-          -- Drop clients that stop answering pings, and clients that fall-          -- so far behind that their event queue fills up.-          -- websockets only sees pongs while reading, hence the reader.-          withPingPong pingPong conn $ \c ->-            let send_events =-                  next >>= \case-                    Nothing ->-                      WebSockets.sendClose c ("Event queue full" :: Text)-                    Just event -> do-                      sendTextData c (A.encode event)-                      send_events-             in race_-                  (loopForever (WebSockets.receiveDataMessage c))-                  send_events-        else-          rejectRequestWith-            pending-            WebSockets.defaultRejectRequest-              { WebSockets.rejectBody =-                  L.toStrict $ A.encode ThingNotFound,-                WebSockets.rejectCode =-                  404,-                WebSockets.rejectMessage =-                  "Not Found",-                WebSockets.rejectHeaders =-                  [("Content-Type", "application/json")]-              }---- | Handler for @GET \/events@: stream of new blocks and mempool transactions, one per line in JSON, until the client falls more than 10000 events behind.-scottyEvents :: (MonadUnliftIO m) => ActionT m ()-scottyEvents =-  withGaugeIncrease (.events) $ do-    setHeaders-    proto <- setupContentType False-    pub <- askl (.config.store.pub)-    S.stream $ \io flush' ->-      withEventSubscription pub $ \next ->-        -- ends the response when the client falls too far behind-        let send_events = do-              flush'-              next >>= \case-                Nothing -> return ()-                Just msg -> io (serial proto msg) >> send_events-         in send_events-  where-    serial proto e =-      lazyByteString $-        protoSerial proto toEncoding toJSON e <> newLine proto-    newLine SerialAsBinary = mempty-    newLine SerialAsJSON = "\n"-    newLine SerialAsPrettyJSON = mempty---- | Most events queued for one /events client. A client that does not keep--- up is disconnected once this many are waiting, instead of the queue--- growing without bound.-eventQueueSize :: Natural-eventQueueSize = 10000---- | Subscribe to the store events that /events clients see. The action gets--- a blocking read that returns the next event, or 'Nothing' once the queue--- has overflowed, after which the client must be disconnected. Other store--- events are dropped by the publisher instead of being queued.-withEventSubscription ::-  Publisher StoreEvent -> (IO (Maybe Event) -> IO a) -> IO a-withEventSubscription pub f = do-  inbox <- newBoundedInbox eventQueueSize-  overflow <- newTVarIO False-  let listen e = forM_ (toEvent e) $ \ev ->-        mailboxFullSTM inbox >>= \case-          True -> writeTVar overflow True-          False -> sendSTM ev inbox-      next =-        atomically $-          (readTVar overflow >>= checkSTM >> return Nothing)-            `orElse` (Just <$> receiveSTM inbox)-  bracket (subscribe pub listen) (unsubscribe pub) $ \_ -> f next-  where-    toEvent = \case-      StoreBestBlock b -> Just (EventBlock b)-      StoreMempoolNew t -> Just (EventTx t)-      StoreMempoolDelete t -> Just (EventTx t)-      _ -> Nothing---- GET Address Transactions ------ | Handler for @GET \/address\/:address\/transactions@: transactions of an address.-scottyAddrTxs ::-  (MonadUnliftIO m) => GetAddrTxs -> ActionT m [TxRef]-scottyAddrTxs (GetAddrTxs addr pLimits) =-  withMetrics (.addressTx) $-    getAddressTxs addr =<< paramToLimits False pLimits---- | Handler for @GET \/address\/transactions?addresses=…@: transactions of several addresses.-scottyAddrsTxs ::-  (MonadUnliftIO m) => GetAddrsTxs -> ActionT m [TxRef]-scottyAddrsTxs (GetAddrsTxs addrs pLimits) =-  withMetrics (.addressTx) $-    getAddressesTxs addrs =<< paramToLimits False pLimits---- | Handler for @GET \/address\/:address\/transactions\/full@: full transactions of an address.-scottyAddrTxsFull ::-  (MonadUnliftIO m) =>-  GetAddrTxsFull ->-  ActionT m [Transaction]-scottyAddrTxsFull (GetAddrTxsFull addr pLimits) =-  withMetrics (.addressTxFull) $ do-    txs <- getAddressTxs addr =<< paramToLimits True pLimits-    catMaybes <$> getTransactions (map (.txid) txs)---- | Handler for @GET \/address\/transactions\/full?addresses=…@: full transactions of several addresses.-scottyAddrsTxsFull ::-  (MonadUnliftIO m) =>-  GetAddrsTxsFull ->-  ActionT m [Transaction]-scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) =-  withMetrics (.addressTxFull) $ do-    txs <- getAddressesTxs addrs =<< paramToLimits True pLimits-    catMaybes <$> getTransactions (map (.txid) txs)---- | Handler for @GET \/address\/:address\/balance@: balance of an address.-scottyAddrBalance :: (MonadUnliftIO m) => GetAddrBalance -> ActionT m Balance-scottyAddrBalance (GetAddrBalance addr) =-  withMetrics (.addressBalance) $ getDefaultBalance addr---- | Handler for @GET \/address\/balances?addresses=…@: balances of several addresses.-scottyAddrsBalance ::-  (MonadUnliftIO m) => GetAddrsBalance -> ActionT m [Balance]-scottyAddrsBalance (GetAddrsBalance addrs) =-  withMetrics (.addressBalance) $ getBalances addrs---- | Handler for @GET \/address\/:address\/unspent@: unspent outputs of an address.-scottyAddrUnspent ::-  (MonadUnliftIO m) => GetAddrUnspent -> ActionT m [Unspent]-scottyAddrUnspent (GetAddrUnspent addr pLimits) =-  withMetrics (.addressUnspent) $-    getAddressUnspents addr =<< paramToLimits False pLimits---- | Handler for @GET \/address\/unspent?addresses=…@: unspent outputs of several addresses.-scottyAddrsUnspent ::-  (MonadUnliftIO m) => GetAddrsUnspent -> ActionT m [Unspent]-scottyAddrsUnspent (GetAddrsUnspent addrs pLimits) =-  withMetrics (.addressUnspent) $-    getAddressesUnspents addrs =<< paramToLimits False pLimits---- GET XPubs ------ | Handler for @GET \/xpub\/:xpub@: summary of an extended public key.-scottyXPub ::-  (MonadUnliftIO m) => GetXPub -> ActionT m XPubSummary-scottyXPub (GetXPub xpub deriv (NoCache noCache)) =-  withMetrics (.xpub) $-    let xs = XPubSpec xpub deriv-     in xPubSummary xs <$> lift (runNoCache noCache $ xPubBals xs)---- | Handler for @DELETE \/xpub\/:xpub@: remove an extended public key from the cache. Returns 'True' if it was cached.-scottyDelXPub ::-  (MonadUnliftIO m) =>-  DelCachedXPub ->-  ActionT m (GenericResult Bool)-scottyDelXPub (DelCachedXPub xpub deriv) =-  withMetrics (.xpubDelete) $ do-    c <- askl (.config.store.cache)-    n <- lift (withCache c $ cacheDelXPubs [XPubSpec xpub deriv])-    return $ GenericResult (n > 0)---- | Transactions of an extended public key, optionally without the cache.-getXPubTxs ::-  (MonadUnliftIO m) =>-  XPubKey ->-  DeriveType ->-  LimitsParam ->-  Bool ->-  ActionT m [TxRef]-getXPubTxs xpub deriv plimits nocache = do-  limits <- paramToLimits False plimits-  let xs = XPubSpec xpub deriv-  xbals <- xPubBals xs-  lift $ runNoCache nocache $ xPubTxs xs xbals limits---- | Handler for @GET \/xpub\/:xpub\/transactions@: transactions of an extended public key.-scottyXPubTxs ::-  (MonadUnliftIO m) => GetXPubTxs -> ActionT m [TxRef]-scottyXPubTxs (GetXPubTxs xpub deriv plimits (NoCache nocache)) =-  withMetrics (.xpubTx) $-    getXPubTxs xpub deriv plimits nocache---- | Handler for @GET \/xpub\/:xpub\/transactions\/full@: full transactions of an extended public key.-scottyXPubTxsFull ::-  (MonadUnliftIO m) =>-  GetXPubTxsFull ->-  ActionT m [Transaction]-scottyXPubTxsFull (GetXPubTxsFull xpub deriv plimits (NoCache nocache)) =-  withMetrics (.xpubTxFull) $-    fmap catMaybes $-      lift . runNoCache nocache . getTransactions . map (.txid)-        =<< getXPubTxs xpub deriv plimits nocache---- | Handler for @GET \/xpub\/:xpub\/balances@: balances of the used addresses of an extended public key.-scottyXPubBalances ::-  (MonadUnliftIO m) => GetXPubBalances -> ActionT m [XPubBal]-scottyXPubBalances (GetXPubBalances xpub deriv (NoCache noCache)) =-  withMetrics (.xpubBalance) $-    lift $-      runNoCache noCache $-        xPubBals (XPubSpec xpub deriv)---- | Handler for @GET \/xpub\/:xpub\/unspent@: unspent outputs of an extended public key.-scottyXPubUnspent ::-  (MonadUnliftIO m) =>-  GetXPubUnspent ->-  ActionT m [XPubUnspent]-scottyXPubUnspent (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) =-  withMetrics (.xpubUnspent) $ do-    limits <- paramToLimits False pLimits-    let xspec = XPubSpec xpub deriv-    xbals <- xPubBals xspec-    lift $ runNoCache noCache $ xPubUnspents xspec xbals limits-------------------------------------------- Blockchain.info API Compatibility ---------------------------------------------- | Currency symbol of the coin of a network, in the Blockchain.info format.-netBinfoSymbol :: Network -> BinfoSymbol-netBinfoSymbol net-  | net == btc =-      BinfoSymbol-        { code = "BTC",-          symbol = "BTC",-          name = "Bitcoin",-          conversion = 100 * 1000 * 1000,-          after = True,-          local = False-        }-  | net == bch =-      BinfoSymbol-        { code = "BCH",-          symbol = "BCH",-          name = "Bitcoin Cash",-          conversion = 100 * 1000 * 1000,-          after = True,-          local = False-        }-  | otherwise =-      BinfoSymbol-        { code = "XTS",-          symbol = "¤",-          name = "Test",-          conversion = 100 * 1000 * 1000,-          after = False,-          local = False-        }---- | Currency symbol of a fiat currency from its price ticker.-binfoTickerToSymbol :: Text -> BinfoTicker -> BinfoSymbol-binfoTickerToSymbol code t =-  BinfoSymbol-    { code,-      symbol = t.symbol,-      name,-      conversion = 100 * 1000 * 1000 / t.fifteen, -- sat/usd-      after = False,-      local = True-    }-  where-    name = case code of-      "EUR" -> "Euro"-      "USD" -> "U.S. dollar"-      "GBP" -> "British pound"-      x -> x---- | Addresses and xpubs in a request parameter, separated by pipes or commas.-getBinfoAddrsParam ::-  (MonadUnliftIO m) =>-  Text ->-  ActionT m (HashSet BinfoAddr)-getBinfoAddrsParam name = do-  net <- askl (.config.store.net)-  ctx <- askl (.config.store.ctx)-  p <- param name `rescue` return ""-  if T.null p-    then return HashSet.empty-    else case parseBinfoAddr net ctx p of-      Nothing -> raise (UserError "invalid address")-      Just xs -> return $ HashSet.fromList xs---- | Xpubs and addresses in the @active@, @activeP2SH@ and @activeBech32@--- parameters. Xpubs in each parameter derive legacy, nested SegWit and native--- SegWit addresses.-getBinfoActive ::-  (MonadUnliftIO m) =>-  ActionT m (HashSet XPubSpec, HashSet Address)-getBinfoActive = do-  active <- getBinfoAddrsParam "active"-  p2sh <- getBinfoAddrsParam "activeP2SH"-  bech32 <- getBinfoAddrsParam "activeBech32"-  let xspec d b = (`XPubSpec` d) <$> xpub b-      xspecs =-        HashSet.fromList $-          concat-            [ mapMaybe (xspec DeriveNormal) (HashSet.toList active),-              mapMaybe (xspec DeriveP2SH) (HashSet.toList p2sh),-              mapMaybe (xspec DeriveP2WPKH) (HashSet.toList bech32)-            ]-      addrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList active-  return (xspecs, addrs)-  where-    addr (BinfoAddr a) = Just a-    addr (BinfoXpub _) = Nothing-    xpub (BinfoXpub x) = Just x-    xpub (BinfoAddr _) = Nothing---- | Whether to identify transactions by short number, unless @txidindex@ is false.-getNumTxId :: (MonadUnliftIO m) => ActionT m Bool-getNumTxId = fmap not $ param "txidindex" `rescue` return False---- | Height of the best header.-getChainHeight :: (MonadUnliftIO m) => ActionT m H.BlockHeight-getChainHeight =-  fmap (.height) $ liftIO . chainGetBest =<< askl (.config.store.chain)---- | Handler for @GET\/POST \/blockchain\/unspent@: unspent outputs of addresses and xpubs, as Blockchain.info.-scottyBinfoUnspent :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoUnspent =-  withMetrics (.binfoUnspent) $ do-    (xspecs, addrs) <- getBinfoActive-    numtxid <- getNumTxId-    limit <- get_limit-    min_conf <- get_min_conf-    net <- askl (.config.store.net)-    ctx <- askl (.config.store.ctx)-    height <- getChainHeight-    let mn u = min_conf > u.confirmations-    xbals <- lift $ getXBals xspecs-    bus <--      lift . runConduit $-        getBinfoUnspents numtxid height xbals xspecs addrs-          .| (dropWhileC mn >> takeC limit .| sinkList)-    setHeaders-    streamEncoding (marshalEncoding (net, ctx) (BinfoUnspents bus))-  where-    get_limit = fmap (min 1000) $ param "limit" `rescue` return 250-    get_min_conf = param "confirmations" `rescue` return 0---- | Stream the unspent outputs of xpubs and addresses, newest first, in the--- Blockchain.info format.-getBinfoUnspents ::-  (StoreReadExtra m, MonadIO m) =>-  Bool ->-  H.BlockHeight ->-  HashMap XPubSpec [XPubBal] ->-  HashSet XPubSpec ->-  HashSet Address ->-  ConduitT () BinfoUnspent m ()-getBinfoUnspents numtxid height xbals xspecs addrs = do-  cs' <- conduits-  joinDescStreams cs' .| mapC (uncurry binfo)-  where-    binfo u xp =-      let conf = case u.block of-            MemRef _ -> 0-            BlockRef h _ -> height - h + 1-       in BinfoUnspent-            { txid = u.outpoint.hash,-              index = u.outpoint.index,-              script = u.script,-              value = u.value,-              confirmations = fromIntegral conf,-              txidx = encodeBinfoTxId numtxid u.outpoint.hash,-              xpub = xp-            }-    conduits = (<>) <$> xconduits <*> pure acounduits-    xconduits = lift $ do-      let f x (XPubUnspent u p) =-            let path = toSoft (listToPath p)-                xp = BinfoXPubPath x.key <$> path-             in (u, xp)-          g x = do-            let h = xPubUnspents x (xBals x xbals)-                l = let Limits {..} = def in Limits {limit = 16, ..}-            return $ streamThings h Nothing l .| mapC (f x)-      mapM g (HashSet.toList xspecs)-    acounduits =-      let f u = (u, Nothing)-          h = getAddressUnspents-          l = let Limits {..} = def in Limits {limit = 16, ..}-          g a = streamThings (h a) Nothing l .| mapC f-       in map g (HashSet.toList addrs)---- | Balances of the used addresses of each xpub.-getXBals ::-  (StoreReadExtra m) =>-  HashSet XPubSpec ->-  m (HashMap XPubSpec [XPubBal])-getXBals =-  fmap HashMap.fromList . mapM f . HashSet.toList-  where-    f x = (x,) . filter (not . nullBalance . (.balance)) <$> xPubBals x---- | Balances of an xpub, or none if missing.-xBals :: XPubSpec -> HashMap XPubSpec [XPubBal] -> [XPubBal]-xBals = HashMap.findWithDefault []---- | Stream the transactions of xpubs and addresses, newest first, in the--- Blockchain.info format, with the running balance of the given addresses after--- each, keeping only those that pass the filter.-getBinfoTxs ::-  (StoreReadExtra m, MonadIO m) =>-  HashMap XPubSpec [XPubBal] -> -- xpub balances-  HashMap Address (Maybe BinfoXPubPath) -> -- address book-  HashSet XPubSpec -> -- show xpubs-  HashSet Address -> -- show addrs-  HashSet Address -> -- balance addresses-  BinfoFilter ->-  Bool -> -- numtxid-  Bool -> -- prune outputs-  Int64 -> -- starting balance-  ConduitT () BinfoTx m ()-getBinfoTxs-  xbals-  abook-  sxspecs-  saddrs-  baddrs-  bfilter-  numtxid-  prune-  bal = do-    cs' <- conduits-    joinDescStreams cs' .| go bal-    where-      sxspecs_ls = HashSet.toList sxspecs-      saddrs_ls = HashSet.toList saddrs-      conduits =-        (<>)-          <$> mapM xpub_c sxspecs_ls-          <*> pure (map addr_c saddrs_ls)-      xpub_c x = do-        let f = xPubTxs x (xBals x xbals)-            l = Limits {limit = 16, offset = 0, start = Nothing}-        lift . return $-          streamThings f (Just (.txid)) l-      addr_c a = do-        let l = Limits {limit = 16, offset = 0, start = Nothing}-        streamThings (getAddressTxs a) (Just (.txid)) l-      binfo_tx = toBinfoTx numtxid abook prune-      compute_bal_change t =-        let ins = map (.output) t.inputs-            out = t.outputs-            f b BinfoTxOutput {..} =-              let val = fromIntegral value-               in case address of-                    Nothing -> 0-                    Just a-                      | a `HashSet.member` baddrs ->-                          if b then val else negate val-                      | otherwise -> 0-         in sum $ map (f False) ins <> map (f True) out-      go b =-        await >>= \case-          Nothing -> return ()-          Just (TxRef _ t) ->-            lift (getTransaction t) >>= \case-              Nothing -> go b-              Just x -> do-                let a = binfo_tx b x-                    b' = b - compute_bal_change a-                    c = isJust a.blockHeight-                    d = fst (fromJust a.balance)-                    r = d + fromIntegral a.fee-                case bfilter of-                  BinfoFilterAll ->-                    yield a >> go b'-                  BinfoFilterSent-                    | 0 > r -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterReceived-                    | r > 0 -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterMoved-                    | r == 0 -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterConfirmed-                    | c -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterMempool-                    | c -> return ()-                    | otherwise -> yield a >> go b'---- | Whether to show addresses in CashAddr format (@cashaddr@).-getCashAddr :: (MonadUnliftIO m) => ActionT m Bool-getCashAddr = param "cashaddr" `rescue` return False---- | Parse an address. Responds with not found if invalid.-getAddress :: (MonadUnliftIO m) => Text -> ActionT m Address-getAddress txt = do-  net <- askl (.config.store.net)-  case textToAddr net txt of-    Nothing -> raise ThingNotFound-    Just a -> return a---- | Address or xpub in a path parameter. Passes to the next route if invalid.-getBinfoAddr :: (MonadUnliftIO m) => Text -> ActionT m BinfoAddr-getBinfoAddr param' = do-  txt <- S.captureParam param'-  net <- askl (.config.store.net)-  ctx <- askl (.config.store.ctx)-  let addr = BinfoAddr <$> textToAddr net txt-      xpub = BinfoXpub <$> xPubImport net ctx txt-  maybe S.next return (addr <|> xpub)---- | Handler for @GET\/POST \/blockchain\/export-history@: daily history of the balance of addresses and xpubs with prices, as Blockchain.info.-scottyBinfoHistory :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoHistory =-  withMetrics (.binfoExportHistory) $ do-    (xspecs, addrs) <- getBinfoActive-    (startM, endM) <- get_dates-    (code, price') <- getPrice-    xbals <- getXBals xspecs-    let xaddrs = HashSet.fromList $ concatMap (map get_addr) (HashMap.elems xbals)-        aaddrs = xaddrs <> addrs-        cur = price'.fifteen-        cs' = conduits (HashMap.toList xbals) addrs endM-    txs <--      lift $-        runConduit $-          joinDescStreams cs'-            .| takeWhileC (is_newer startM)-            .| concatMapMC get_transaction-            .| sinkList-    let times = map (\Transaction {..} -> timestamp) txs-    net <- askl (.config.store.net)-    url <- askl (.config.priceHistoryURL)-    session <- askl (.session)-    logger <- askl (.config.logger)-    rates <- map (.price) <$> lift (getRates logger net session url code times)-    let hs = zipWith (convert cur aaddrs) txs (rates <> repeat 0.0)-    setHeaders-    streamEncoding $ toEncoding hs-  where-    is_newer-      (Just BlockData {height = bh})-      TxRef {block = BlockRef {height = th}} =-        bh <= th-    is_newer Nothing TxRef {} = True-    is_newer _ _ = undefined-    get_addr = (.balance.address)-    get_transaction TxRef {txid = h} =-      getTransaction h-    convert cur addrs tx rate =-      let ins = tx.inputs-          outs = tx.outputs-          fins = filter (input_addr addrs) ins-          fouts = filter (output_addr addrs) outs-          vin = fromIntegral . sum $ map (.value) fins-          vout = fromIntegral . sum $ map (.value) fouts-          fee = tx.fee-          v = vout - vin-          t = tx.timestamp-          h = txHash $ transactionData tx-       in toBinfoHistory v t rate cur fee h-    input_addr addrs' StoreInput {address = Just a} =-      a `HashSet.member` addrs'-    input_addr _ _ = False-    output_addr addrs' StoreOutput {address = Just a} =-      a `HashSet.member` addrs'-    output_addr _ _ = False-    get_dates = do-      BinfoDate start <- param "start"-      BinfoDate end' <- param "end"-      let end = end' + 24 * 60 * 60-      ch <- askl (.config.store.chain)-      startM <- blockAtOrAfter ch start-      endM <- blockAtOrBefore ch end-      return (startM, endM)-    conduits xpubs addrs endM =-      map (uncurry (xpub_c endM)) xpubs-        <> map (addr_c endM) (HashSet.toList addrs)-    lim endM =-      Limits-        { limit = 16,-          offset = 0,-          start = AtBlock . (.height) <$> endM-        }-    addr_c endM a =-      streamThings (getAddressTxs a) (Just (.txid)) (lim endM)-    xpub_c endM x bs =-      streamThings (xPubTxs x bs) (Just (.txid)) (lim endM)---- | Currency code in the @currency@ parameter, USD by default, and its latest price.-getPrice :: (MonadUnliftIO m) => ActionT m (Text, BinfoTicker)-getPrice = do-  code <- T.toUpper <$> param "currency" `rescue` return "USD"-  ticker <- askl (.ticker)-  prices <- readTVarIO ticker-  case HashMap.lookup code prices of-    Nothing -> return (code, def)-    Just p -> return (code, p)---- | Symbol of the currency in the @currency@ parameter.-getSymbol :: (MonadUnliftIO m) => ActionT m BinfoSymbol-getSymbol = uncurry binfoTickerToSymbol <$> getPrice---- | Handler for @GET \/blockchain\/blocks\/:milliseconds@: the blocks of the day before a time in milliseconds, as Blockchain.info.-scottyBinfoBlocksDay :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoBlocksDay =-  withMetrics (.binfoBlock) $ do-    t <- min h . (`div` 1000) <$> S.captureParam "milliseconds"-    ch <- askl (.config.store.chain)-    m <- blockAtOrBefore ch t-    bs <- go (d t) m-    streamEncoding $ toEncoding $ map toBinfoBlockInfo bs-  where-    h = fromIntegral (maxBound :: H.Timestamp)-    d = subtract (24 * 3600)-    go _ Nothing = return []-    go t (Just b)-      | b.header.timestamp <= fromIntegral t =-          return []-      | otherwise = do-          b' <- getBlock b.header.prev-          (b :) <$> go t b'---- | Handler for @GET\/POST \/blockchain\/multiaddr@: balances and transactions of addresses and xpubs, as Blockchain.info.-scottyMultiAddr :: (MonadUnliftIO m) => ActionT m ()-scottyMultiAddr =-  withMetrics (.binfoMultiaddr) $ do-    (addrs', _, saddrs, sxpubs, xspecs) <- get_addrs-    numtxid <- getNumTxId-    cashaddr <- getCashAddr-    local' <- getSymbol-    offset <- getBinfoOffset-    n <- getBinfoCount "n"-    prune <- get_prune-    fltr <- get_filter-    xbals <- getXBals xspecs-    xtxns <- get_xpub_tx_count xbals xspecs-    let sxbals = only_show_xbals sxpubs xbals-        xabals = compute_xabals xbals-        addrs = addrs' `HashSet.difference` HashMap.keysSet xabals-    abals <- get_abals addrs-    let sxspecs = only_show_xspecs sxpubs xspecs-        sxabals = compute_xabals sxbals-        sabals = only_show_abals saddrs abals-        sallbals = sabals <> sxabals-        sbal = compute_bal sallbals-        allbals = abals <> xabals-        abook = compute_abook addrs xbals-        sxaddrs = compute_xaddrs sxbals-        salladdrs = saddrs <> sxaddrs-        bal = compute_bal allbals-        ibal = fromIntegral sbal-    ftxs <--      lift . runConduit $-        getBinfoTxs-          xbals-          abook-          sxspecs-          saddrs-          salladdrs-          fltr-          numtxid-          prune-          ibal-          .| (dropC offset >> takeC n .| sinkList)-    net <- askl (.config.store.net)-    ctx <- askl (.config.store.ctx)-    best <- get_best_block-    peers <- get_peers-    let baddrs = toBinfoAddrs sabals sxbals xtxns-        abaddrs = toBinfoAddrs abals xbals xtxns-        recv = sum $ map (.received) abaddrs-        sent' = sum $ map (.sent) abaddrs-        txn = fromIntegral $ length ftxs-        wallet =-          BinfoWallet-            { balance = bal,-              txs = txn,-              filtered = txn,-              received = recv,-              sent = sent'-            }-        coin = netBinfoSymbol net-    let block =-          BinfoBlockInfo-            { hash = H.headerHash best.header,-              height = best.height,-              timestamp = best.header.timestamp,-              index = best.height-            }-    let info =-          BinfoInfo-            { connected = peers,-              conversion = 100 * 1000 * 1000,-              fiat = local',-              crypto = coin,-              head = block-            }-    let multiaddr =-          BinfoMultiAddr-            { addresses = baddrs,-              wallet = wallet,-              txs = ftxs,-              info = info,-              recommendFee = True,-              cashAddr = cashaddr-            }-    setHeaders-    streamEncoding $ marshalEncoding (net, ctx) multiaddr-  where-    get_xpub_tx_count xbals =-      fmap HashMap.fromList-        . mapM-          ( \x ->-              (x,)-                . fromIntegral-                <$> xPubTxCount x (xBals x xbals)-          )-        . HashSet.toList-    get_filter = param "filter" `rescue` return BinfoFilterAll-    get_best_block =-      getBestBlock >>= \case-        Nothing -> raise ThingNotFound-        Just bh ->-          getBlock bh >>= \case-            Nothing -> raise ThingNotFound-            Just b -> return b-    get_prune =-      fmap not $-        param "noCompact"-          `rescue` param "no_compact"-          `rescue` return False-    only_show_xbals sxpubs =-      HashMap.filterWithKey (\k _ -> k.key `HashSet.member` sxpubs)-    only_show_xspecs sxpubs =-      HashSet.filter (\k -> k.key `HashSet.member` sxpubs)-    only_show_abals saddrs =-      HashMap.filterWithKey (\k _ -> k `HashSet.member` saddrs)-    addr (BinfoAddr a) = Just a-    addr (BinfoXpub _) = Nothing-    xpub (BinfoXpub x) = Just x-    xpub (BinfoAddr _) = Nothing-    get_addrs = do-      (xspecs, addrs) <- getBinfoActive-      sh <- getBinfoAddrsParam "onlyShow"-      let xpubs = HashSet.map (.key) xspecs-          actives =-            HashSet.map BinfoAddr addrs-              <> HashSet.map BinfoXpub xpubs-          sh' = if HashSet.null sh then actives else sh-          saddrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList sh'-          sxpubs = HashSet.fromList $ mapMaybe xpub $ HashSet.toList sh'-      return (addrs, xpubs, saddrs, sxpubs, xspecs)-    get_abals =-      let f b = (b.address, b)-          g = HashMap.fromList . map f-       in fmap g . getBalances . HashSet.toList-    get_peers = do-      ps <- lift $ getPeersInformation =<< asks (.config.store.peerMgr)-      return (fromIntegral (length ps))-    compute_xabals =-      let f b = (b.balance.address, b.balance)-       in HashMap.fromList . concatMap (map f) . HashMap.elems-    compute_bal =-      let f b = b.confirmed + b.unconfirmed-       in sum . map f . HashMap.elems-    compute_abook addrs xbals =-      let f xs xb =-            let a = xb.balance.address-                e = error "lions and tigers and bears"-                s = toSoft (listToPath xb.path)-             in (a, Just (BinfoXPubPath xs.key (fromMaybe e s)))-          amap =-            HashMap.map (const Nothing) $-              HashSet.toMap addrs-          xmap =-            HashMap.fromList-              . concatMap (uncurry (map . f))-              $ HashMap.toList xbals-       in amap <> xmap-    compute_xaddrs =-      let f = map (.balance.address)-       in HashSet.fromList . concatMap f . HashMap.elems---- | Number of items requested in a parameter, within the limits.-getBinfoCount :: (MonadUnliftIO m) => Text -> ActionT m Int-getBinfoCount str = do-  d <- askl (.config.limits.defItemCount)-  x <- askl (.config.limits.maxFullItemCount)-  i <- min x <$> (param str `rescue` return d)-  return (fromIntegral i :: Int)---- | Offset requested in the @offset@ parameter. Responds with an error if above the maximum.-getBinfoOffset ::-  (MonadUnliftIO m) =>-  ActionT m Int-getBinfoOffset = do-  x <- askl (.config.limits.maxOffset)-  o <- param "offset" `rescue` return 0-  when (o > x) $-    raise $-      UserError $-        "offset exceeded: " <> show o <> " > " <> show x-  return (fromIntegral o :: Int)---- | Handler for @GET \/blockchain\/rawaddr\/:addr@: balance and transactions of an address or xpub, as Blockchain.info; also at @\/blockchain\/address\/:addr@ and @\/blockchain\/xpub\/:addr@.-scottyRawAddr :: (MonadUnliftIO m) => ActionT m ()-scottyRawAddr =-  withMetrics (.binfoAddressRaw) $-    getBinfoAddr "addr" >>= \case-      BinfoAddr addr -> do_addr addr-      BinfoXpub xpub -> do_xpub xpub-  where-    do_xpub xpub = do-      numtxid <- getNumTxId-      derive <- param "derive" `rescue` return DeriveNormal-      let xspec = XPubSpec xpub derive-      n <- getBinfoCount "limit"-      off <- getBinfoOffset-      xbals <- getXBals $ HashSet.singleton xspec-      net <- askl (.config.store.net)-      let summary = xPubSummary xspec (xBals xspec xbals)-          abook = compute_abook xpub (xBals xspec xbals)-          xspecs = HashSet.singleton xspec-          saddrs = HashSet.empty-          baddrs = HashMap.keysSet abook-          bfilter = BinfoFilterAll-          amnt = summary.confirmed + summary.unconfirmed-      txs <--        lift $-          runConduit $-            getBinfoTxs-              xbals-              abook-              xspecs-              saddrs-              baddrs-              bfilter-              numtxid-              False-              (fromIntegral amnt)-              .| (dropC off >> takeC n .| sinkList)-      let ra =-            BinfoRawAddr-              { address = BinfoXpub xpub,-                balance = amnt,-                ntx = fromIntegral $ length txs,-                utxo = summary.utxo,-                received = summary.received,-                sent = fromIntegral summary.received - fromIntegral amnt,-                txs = txs-              }-      setHeaders-      ctx <- askl (.config.store.ctx)-      streamEncoding $ marshalEncoding (net, ctx) ra-    compute_abook xpub xbals =-      let f xb =-            let a = xb.balance.address-                e = error "black hole swallows all your code"-                s = toSoft $ listToPath xb.path-                m = fromMaybe e s-             in (a, Just (BinfoXPubPath xpub m))-       in HashMap.fromList $ map f xbals-    do_addr addr = do-      numtxid <- getNumTxId-      n <- getBinfoCount "limit"-      off <- getBinfoOffset-      bal <- fromMaybe (zeroBalance addr) <$> getBalance addr-      net <- askl (.config.store.net)-      let abook = HashMap.singleton addr Nothing-          xspecs = HashSet.empty-          saddrs = HashSet.singleton addr-          bfilter = BinfoFilterAll-          balance = bal.confirmed + bal.unconfirmed-      txs <--        lift . runConduit $-          getBinfoTxs-            HashMap.empty-            abook-            xspecs-            saddrs-            saddrs-            bfilter-            numtxid-            False-            (fromIntegral balance)-            .| (dropC off >> takeC n .| sinkList)-      let ra =-            BinfoRawAddr-              { address = BinfoAddr addr,-                balance,-                ntx = bal.txs,-                utxo = bal.utxo,-                received = bal.received,-                sent = fromIntegral bal.received - fromIntegral balance,-                txs-              }-      setHeaders-      ctx <- askl (.config.store.ctx)-      streamEncoding $ marshalEncoding (net, ctx) ra---- | Handler for @GET \/blockchain\/q\/getreceivedbyaddress\/:addr@: total received by an address.-scottyBinfoReceived :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoReceived =-  withMetrics (.binfoQgetreceivedbyaddress) $ do-    a <- getAddress =<< S.captureParam "addr"-    b <- fromMaybe (zeroBalance a) <$> getBalance a-    setHeaders-    S.text $ cs $ show b.received---- | Handler for @GET \/blockchain\/q\/getsentbyaddress\/:addr@: total sent by an address.-scottyBinfoSent :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoSent =-  withMetrics (.binfoQgetsentbyaddress) $ do-    a <- getAddress =<< S.captureParam "addr"-    b <- fromMaybe (zeroBalance a) <$> getBalance a-    setHeaders-    S.text $ cs $ show $ b.received - b.confirmed - b.unconfirmed---- | Handler for @GET \/blockchain\/q\/addressbalance\/:addr@: balance of an address.-scottyBinfoAddrBalance :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoAddrBalance =-  withMetrics (.binfoQaddressbalance) $ do-    a <- getAddress =<< S.captureParam "addr"-    b <- fromMaybe (zeroBalance a) <$> getBalance a-    setHeaders-    S.text $ cs $ show $ b.confirmed + b.unconfirmed---- | Handler for @GET \/blockchain\/q\/addressfirstseen\/:addr@: time of the first block with a transaction of an address.-scottyFirstSeen :: (MonadUnliftIO m) => ActionT m ()-scottyFirstSeen =-  withMetrics (.binfoQaddressfirstseen) $ do-    a <- getAddress =<< S.captureParam "addr"-    ch <- askl (.config.store.chain)-    bb <- liftIO $ chainGetBest ch-    let top = bb.height-        bot = 0-    i <- go ch bb a bot top-    setHeaders-    S.text $ cs $ show i-  where-    go ch bb a bot top = do-      let mid = bot + (top - bot) `div` 2-          n = top - bot < 2-      x <- hasone a bot-      y <- hasone a mid-      z <- hasone a top-      if-        | x -> getblocktime ch bb bot-        | n -> getblocktime ch bb top-        | y -> go ch bb a bot mid-        | z -> go ch bb a mid top-        | otherwise -> return 0-    getblocktime ch bb h =-      liftIO (chainGetAncestor ch h bb) >>= \case-        Just b -> return b.header.timestamp-        Nothing -> do-          logger <- askl (.config.logger)-          logError logger $-            "Could not get ancestor at height "-              <> cs (show h)-              <> " for block "-              <> H.blockHashToHex (H.headerHash bb.header)-          error "Block ancestor retreival error"-    hasone a h = do-      let l = Limits 1 0 (Just (AtBlock h))-      not . null <$> getAddressTxs a l---- | Handler for @GET\/POST \/blockchain\/balance@: short balances of addresses and xpubs, as Blockchain.info.-scottyShortBal :: (MonadUnliftIO m) => ActionT m ()-scottyShortBal =-  withMetrics (.binfoBalance) $ do-    (xspecs, addrs) <- getBinfoActive-    cashaddr <- getCashAddr-    net <- askl (.config.store.net)-    abals <- catMaybes <$> mapM (getabal net cashaddr) (HashSet.toList addrs)-    xbals <- mapM (getxbal net) (HashSet.toList xspecs)-    let res = HashMap.fromList (abals <> xbals)-    setHeaders-    streamEncoding $ toEncoding res-  where-    shorten bal =-      BinfoShortBal-        { final = bal.confirmed + bal.unconfirmed,-          ntx = bal.txs,-          received = bal.received-        }-    getabal net cashaddr a =-      let net' =-            if-              | cashaddr -> net-              | net == bch -> btc-              | net == bchTest -> btcTest-              | net == bchTest4 -> btcTest-              | otherwise -> net-       in case addrToText net' a of-            Nothing -> return Nothing-            Just a' ->-              getBalance a >>= \case-                Nothing -> return $ Just (a', shorten (zeroBalance a))-                Just b -> return $ Just (a', shorten b)-    getxbal net xpub = do-      xbals <- xPubBals xpub-      ntx <- fromIntegral <$> xPubTxCount xpub xbals-      let val = sum $ map (.balance.confirmed) xbals-          zro = sum $ map (.balance.unconfirmed) xbals-          exs = [x | x@XPubBal {path = 0 : _} <- xbals]-          final = val + zro-          received = sum $ map (.balance.received) exs-          sbl = BinfoShortBal {final, ntx, received}-      ctx <- askl (.config.store.ctx)-      return (xPubExport net ctx xpub.key, sbl)---- | Whether to respond with hex (@format=hex@).-getBinfoHex :: (MonadUnliftIO m) => ActionT m Bool-getBinfoHex = (== ("hex" :: Text)) <$> param "format" `rescue` return "json"---- | Handler for @GET \/blockchain\/block-height\/:height@: the blocks at a height, as Blockchain.info.-scottyBinfoBlockHeight :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoBlockHeight =-  withMetrics (.binfoBlockHeight) $ do-    numtxid <- getNumTxId-    height <- S.captureParam "height"-    bs <- fmap catMaybes $ getBlocksAtHeight height >>= mapM getBlock-    ns <- fmap catMaybes $ getBlocksAtHeight (height + 1) >>= mapM getBlock-    is <- mapM (get_binfo_blocks numtxid ns) bs-    setHeaders-    net <- askl (.config.store.net)-    ctx <- askl (.config.store.ctx)-    streamEncoding $ marshalEncoding (net, ctx) is-  where-    get_binfo_blocks numtxid ns b = do-      txs <- catMaybes <$> getTransactions b.txs-      let h x = H.headerHash x.header-          nbs = [h n | n <- ns, n.header.prev == h b]-          bts = map (toBinfoTxSimple numtxid) txs-      return $ toBinfoBlock b bts nbs---- | Handler for @GET \/blockchain\/latestblock@: the best block, as Blockchain.info.-scottyBinfoLatest :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoLatest =-  withMetrics (.binfoBlockLatest) $ do-    numtxid <- getNumTxId-    mb <- runMaybeT $ MaybeT getBestBlock >>= MaybeT . getBlock-    b <- maybe (raise ThingNotFound) return mb-    streamEncoding $-      toEncoding-        BinfoHeader-          { hash = H.headerHash b.header,-            timestamp = b.header.timestamp,-            index = b.height,-            height = b.height,-            txids = map (encodeBinfoTxId numtxid) b.txs-          }---- | Handler for @GET \/blockchain\/rawblock\/:block@: a block with its transactions, as Blockchain.info.-scottyBinfoBlock :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoBlock =-  withMetrics (.binfoBlockRaw) $ do-    numtxid <- getNumTxId-    hex <- getBinfoHex-    S.captureParam "block" >>= \case-      BinfoBlockHash bh -> go numtxid hex bh-      BinfoBlockIndex i ->-        getBlocksAtHeight i >>= \case-          [] -> raise ThingNotFound-          bh : _ -> go numtxid hex bh-  where-    go numtxid hex bh = do-      b <- maybe (raise ThingNotFound) return =<< getBlock bh-      txs <- catMaybes <$> getTransactions b.txs-      nhs <- fmap catMaybes $ getBlocksAtHeight (b.height + 1) >>= mapM getBlock-      let h x = H.headerHash x.header-          nxt = [h n | n <- nhs, n.header.prev == h b]-      if hex-        then do-          let x = H.Block b.header (map transactionData txs)-          setHeaders-          S.text . encodeHexLazy . runPutL $ serialize x-        else do-          let bts = map (toBinfoTxSimple numtxid) txs-              y = toBinfoBlock b bts nxt-          setHeaders-          net <- askl (.config.store.net)-          ctx <- askl (.config.store.ctx)-          streamEncoding $ marshalEncoding (net, ctx) y---- | Transaction by hash or short number. Fails with 'TxIndexConflict' if a short number matches several.-getBinfoTx ::-  (MonadUnliftIO m) =>-  BinfoTxId ->-  ActionT m (Either Except Transaction)-getBinfoTx txid = do-  tx <- case txid of-    BinfoTxIdHash h -> maybeToList <$> getTransaction h-    BinfoTxIdIndex i -> getNumTransaction i-  case tx of-    [t] -> return $ Right t-    [] -> return $ Left ThingNotFound-    ts ->-      let tids = map (txHash . transactionData) ts-       in return $ Left (TxIndexConflict tids)---- | Handler for @GET \/blockchain\/rawtx\/:txid@: a transaction, as Blockchain.info.-scottyBinfoTx :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoTx =-  withMetrics (.binfoTxRaw) $ do-    numtxid <- getNumTxId-    hex <- getBinfoHex-    txid <- S.captureParam "txid"-    tx <--      getBinfoTx txid >>= \case-        Right t -> return t-        Left e -> raise e-    if hex then hx tx else js numtxid tx-  where-    js numtxid t = do-      net <- askl (.config.store.net)-      ctx <- askl (.config.store.ctx)-      setHeaders-      streamEncoding $ marshalEncoding (net, ctx) $ toBinfoTxSimple numtxid t-    hx t = do-      setHeaders-      S.text . encodeHexLazy . runPutL . serialize $ transactionData t---- | Handler for @GET \/blockchain\/q\/txtotalbtcoutput\/:txid@: total value of the outputs of a transaction.-scottyBinfoTotalOut :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoTotalOut =-  withMetrics (.binfoQtxtotalbtcoutput) $ do-    txid <- S.captureParam "txid"-    tx <--      getBinfoTx txid >>= \case-        Right t -> return t-        Left e -> raise e-    S.text $ cs $ show $ sum $ map (.value) tx.outputs---- | Handler for @GET \/blockchain\/q\/txfee\/:txid@: fee of a transaction.-scottyBinfoTxFees :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoTxFees =-  withMetrics (.binfoQtxfee) $ do-    txid <- S.captureParam "txid"-    tx <--      getBinfoTx txid >>= \case-        Right t -> return t-        Left e -> raise e-    let i = sum $ map (.value) $ filter f tx.inputs-        o = sum $ map (.value) tx.outputs-    S.text . cs . show $ i - o-  where-    f StoreInput {} = True-    f StoreCoinbase {} = False---- | Handler for @GET \/blockchain\/q\/txresult\/:txid\/:addr@: net amount that a transaction sends to an address.-scottyBinfoTxResult :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoTxResult =-  withMetrics (.binfoQtxresult) $ do-    txid <- S.captureParam "txid"-    addr <- getAddress =<< S.captureParam "addr"-    tx <--      getBinfoTx txid >>= \case-        Right t -> return t-        Left e -> raise e-    let i = toInteger $ sum $ map (.value) $ filter (f addr) tx.inputs-        o = toInteger $ sum $ map (.value) $ filter (g addr) tx.outputs-    S.text $ cs $ show $ o - i-  where-    f addr StoreInput {address = Just a} = a == addr-    f _ _ = False-    g addr StoreOutput {address = Just a} = a == addr-    g _ _ = False---- | Handler for @GET \/blockchain\/q\/txtotalbtcinput\/:txid@: total value of the inputs of a transaction.-scottyBinfoTotalInput :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoTotalInput =-  withMetrics (.binfoQtxtotalbtcinput) $ do-    txid <- S.captureParam "txid"-    tx <--      getBinfoTx txid >>= \case-        Right t -> return t-        Left e -> raise e-    S.text $ cs $ show $ sum $ map (.value) $ filter f $ tx.inputs-  where-    f StoreInput {} = True-    f StoreCoinbase {} = False---- | Handler for @GET \/blockchain\/unconfirmed-transactions@: mempool transactions, as Blockchain.info.-scottyBinfoMempool :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoMempool =-  withMetrics (.binfoMempool) $ do-    numtxid <- getNumTxId-    offset <- getBinfoOffset-    n <- getBinfoCount "limit"-    -- a limit of 0 would mean no limit to getMempoolPage-    txids <--      if n <= 0-        then return []-        else-          map snd-            <$> getMempoolPage (Limits (fromIntegral n) (fromIntegral offset) Nothing)-    txs <- catMaybes <$> getTransactions txids-    net <- askl (.config.store.net)-    setHeaders-    let mem = BinfoMempool $ map (toBinfoTxSimple numtxid) txs-    ctx <- askl (.config.store.ctx)-    streamEncoding $ marshalEncoding (net, ctx) mem---- | Handler for @GET \/blockchain\/q\/getblockcount@: height of the best header.-scottyBinfoGetBlockCount :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoGetBlockCount =-  withMetrics (.binfoQgetblockcount) $ do-    ch <- askl (.config.store.chain)-    bn <- liftIO $ chainGetBest ch-    setHeaders-    S.text $ TL.pack $ show bn.height---- | Handler for @GET \/blockchain\/q\/latesthash@: hash of the best header.-scottyBinfoLatestHash :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoLatestHash =-  withMetrics (.binfoQlatesthash) $ do-    ch <- askl (.config.store.chain)-    bn <- liftIO $ chainGetBest ch-    setHeaders-    S.text $ TL.fromStrict $ H.blockHashToHex $ H.headerHash bn.header---- | Handler for @GET \/blockchain\/q\/bcperblock@: block subsidy at the next height.-scottyBinfoSubsidy :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoSubsidy =-  withMetrics (.binfoQbcperblock) $ do-    ch <- askl (.config.store.chain)-    net <- askl (.config.store.net)-    bn <- liftIO $ chainGetBest ch-    setHeaders-    S.text $-      cs $-        show $-          (/ (100 * 1000 * 1000 :: Double)) $-            fromIntegral $-              H.computeSubsidy net (bn.height + 1)---- | Handler for @GET \/blockchain\/q\/addresstohash\/:addr@: hash160 of an address in hex.-scottyBinfoAddrToHash :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoAddrToHash =-  withMetrics (.binfoQaddresstohash) $ do-    addr <- getAddress =<< S.captureParam "addr"-    setHeaders-    if-      | Just h <- addressHash160 addr ->-          S.text $ encodeHexLazy $ runPutL $ serialize h-      | Just h <- addressHash256 addr ->-          S.text $ encodeHexLazy $ runPutL $ serialize h-      | otherwise ->-          return ()---- | Handler for @GET \/blockchain\/q\/hashtoaddress\/:hash@: pay-to-public-key-hash address of a hash160.-scottyBinfoHashToAddr :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoHashToAddr =-  withMetrics (.binfoQhashtoaddress) $ do-    bs <- maybe S.next return . decodeHex =<< S.captureParam "hash"-    net <- askl (.config.store.net)-    hash <- either (const S.next) return $ decode bs-    addr <- maybe S.next return $ addrToText net $ PubKeyAddress hash-    setHeaders-    S.text $ TL.fromStrict addr---- | Handler for @GET \/blockchain\/q\/addrpubkey\/:pubkey@: address of a public key.-scottyBinfoAddrPubkey :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoAddrPubkey =-  withMetrics (.binfoQaddrpubkey) $ do-    hex <- S.captureParam "pubkey"-    ctx <- askl (.config.store.ctx)-    pubkey <--      maybe S.next (return . pubKeyAddr ctx) $-        eitherToMaybe . unmarshal ctx =<< decodeHex hex-    net <- askl (.config.store.net)-    setHeaders-    case addrToText net pubkey of-      Nothing -> raise ThingNotFound-      Just a -> S.text $ TL.fromStrict a---- | Handler for @GET \/blockchain\/q\/pubkeyaddr\/:addr@: public key of an address, found in a transaction input that spends from it.-scottyBinfoPubKeyAddr :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoPubKeyAddr =-  withMetrics (.binfoQpubkeyaddr) $ do-    addr <- getAddress =<< S.captureParam "addr"-    mi <- strm addr-    i <- case mi of-      Nothing -> raise ThingNotFound-      Just i -> return i-    pk <- case extr addr i of-      Left e -> raise $ UserError e-      Right t -> return t-    setHeaders-    S.text $ encodeHexLazy $ L.fromStrict pk-  where-    strm addr = do-      runConduit $ do-        let l = Limits {limit = 8, offset = 0, start = Nothing}-        streamThings (getAddressTxs addr) (Just (.txid)) l-          .| concatMapMC (getTransaction . (.txid))-          .| concatMapC (filter (inp addr) . (.inputs))-          .| headC-    inp addr StoreInput {address = Just a} = a == addr-    inp _ _ = False-    extr addr StoreInput {script, pkscript, witness} = do-      Script sig <- decode script-      Script pks <- decode pkscript-      case addr of-        PubKeyAddress {} ->-          case sig of-            [OP_PUSHDATA _ _, OP_PUSHDATA pub _] ->-              Right pub-            [OP_PUSHDATA _ _] ->-              case pks of-                [OP_PUSHDATA pub _, OP_CHECKSIG] ->-                  Right pub-                _ -> Left "Could not parse scriptPubKey"-            _ -> Left "Could not parse scriptSig"-        WitnessPubKeyAddress {} ->-          case witness of-            [_, pub] -> return pub-            _ -> Left "Could not parse scriptPubKey"-        _ -> Left "Address does not have public key"-    extr _ _ = Left "Incorrect input type"---- | Handler for @GET \/blockchain\/q\/hashpubkey\/:pubkey@: hash160 of a public key in hex.-scottyBinfoHashPubkey :: (MonadUnliftIO m) => ActionT m ()-scottyBinfoHashPubkey =-  withMetrics (.binfoQhashpubkey) $ do-    ctx <- askl (.config.store.ctx)-    pkm <--      fmap-        (eitherToMaybe . unmarshal ctx <=< decodeHex)-        (S.captureParam "pubkey")-    addr <- case pkm of-      Nothing -> raise $ UserError "Could not decode public key"-      Just pk -> return $ pubKeyAddr ctx pk-    setHeaders-    if-      | Just h <- addressHash160 addr ->-          S.text $ encodeHexLazy $ runPutL $ serialize h-      | Just h <- addressHash256 addr ->-          S.text $ encodeHexLazy $ runPutL $ serialize h-      | otherwise ->-          return ()---- GET Network Information ------ | Handler for @GET \/peers@: connected peers.-scottyPeers ::-  (MonadUnliftIO m) =>-  GetPeers ->-  ActionT m [PeerInfo]-scottyPeers _ =-  withMetrics (.peers) $ do-    lift . getPeersInformation =<< askl (.config.store.peerMgr)---- | Obtain information about connected peers from peer manager process.-getPeersInformation ::-  (MonadIO m) => PeerMgr -> m [PeerInfo]-getPeersInformation mgr =-  mapMaybe toInfo <$> liftIO (getPeers mgr)-  where-    toInfo op = do-      ver <- op.version-      return-        PeerInfo-          { userAgent = ver.userAgent.get,-            address = show op.address,-            -- Signed, as the peer sent it. See 'PeerInfo'.-            version = ver.version,-            services = ver.services,-            relay = ver.relay-          }---- | Handler for @GET \/health@: health of the store.-scottyHealth ::-  (MonadUnliftIO m) => GetHealth -> ActionT m HealthCheck-scottyHealth _ =-  withMetrics (.health) $ do-    h <- askl (.health) >>= readTVarIO-    unless (isOK h) $ S.status status503-    return h---- | Heights of the best header and best block, and the maximum lag allowed.-blockHealthCheck ::-  (MonadUnliftIO m, StoreReadBase m) =>-  WebConfig ->-  m BlockHealth-blockHealthCheck cfg = do-  let ch = cfg.store.chain-  headers <- (.height) <$> liftIO (chainGetBest ch)-  blocks <--    maybe 0 (.height)-      <$> runMaybeT (MaybeT getBestBlock >>= MaybeT . getBlock)-  return-    BlockHealth-      { headers,-        blocks,-        max = fromIntegral cfg.maxLaggingBlocks-      }---- | Age of the best header and the maximum allowed.-lastBlockHealthCheck ::-  (MonadUnliftIO m, StoreReadBase m) =>-  Chain ->-  WebLimits ->-  m TimeHealth-lastBlockHealthCheck ch WebLimits {blockTimeout} = do-  n <- round <$> liftIO getPOSIXTime-  t <- fromIntegral . (.header.timestamp) <$> liftIO (chainGetBest ch)-  return-    TimeHealth-      { age = n - t,-        max = fromIntegral blockTimeout-      }---- | Age of the newest mempool transaction or best header, and the maximum allowed.-lastTxHealthCheck ::-  (MonadUnliftIO m, StoreReadBase m) =>-  WebConfig ->-  m TimeHealth-lastTxHealthCheck WebConfig {noMempool, store = store', limits} = do-  n <- round <$> liftIO getPOSIXTime-  b <- fromIntegral . (.header.timestamp) <$> liftIO (chainGetBest ch)-  t <--    getMempoolPage (Limits 1 0 Nothing) >>= \case-      t : _ ->-        let x = fromIntegral $ fst t-         in return $ max x b-      [] -> return b-  return-    TimeHealth-      { age = n - t,-        max = fromIntegral to-      }-  where-    ch = store'.chain-    to =-      if noMempool-        then limits.blockTimeout-        else limits.txTimeout---- | Transactions waiting to be imported and the maximum allowed.-pendingTxsHealthCheck ::-  (MonadUnliftIO m, StoreReadBase m) =>-  WebConfig ->-  m MaxHealth-pendingTxsHealthCheck cfg = do-  n <- blockStorePendingTxs cfg.store.block-  return-    MaxHealth-      { max = fromIntegral cfg.maxPendingTxs,-        count = fromIntegral n-      }---- | Online peers and the minimum required.-peerHealthCheck ::-  (MonadUnliftIO m, StoreReadBase m) =>-  WebConfig ->-  m CountHealth-peerHealthCheck cfg = do-  count <- fromIntegral . length <$> liftIO (getPeers cfg.store.peerMgr)-  return CountHealth {min = fromIntegral cfg.minPeers, count}---- | Run every health check.-healthCheck ::-  (MonadUnliftIO m, StoreReadBase m) => WebConfig -> m HealthCheck-healthCheck cfg = do-  blocks <- blockHealthCheck cfg-  lastBlock <- lastBlockHealthCheck cfg.store.chain cfg.limits-  lastTx <- lastTxHealthCheck cfg-  pendingTxs <- pendingTxsHealthCheck cfg-  peers <- peerHealthCheck cfg-  time <- round <$> liftIO getPOSIXTime-  return-    HealthCheck-      { network = cfg.store.net.name,-        version = cfg.version,-        ..-      }---- | Handler for @GET \/dbstats@: RocksDB memory use and statistics, as text.-scottyDbStats :: (MonadUnliftIO m) => ActionT m ()-scottyDbStats =-  withMetrics (.db) $ do-    setHeaders-    db <- askl (.config.store.db.db)-    memory <- liftIO (memoryReport db)-    statsM <- liftIO (getProperty db Stats)-    S.text . cs $ memory <> maybe "Could not get stats" cs statsM---------------------------- Parameter Parsing ------------------------------ | Returns @Nothing@ if the parameter is not supplied. Raises an exception on--- parse failure.-paramOptional :: forall a m. (Param a, MonadUnliftIO m) => ActionT m (Maybe a)-paramOptional = do-  net <- askl (.config.store.net)-  ctx <- askl (.config.store.ctx)-  fmap Just (param label) `rescue` return Nothing >>= \case-    Nothing -> return Nothing -- Parameter was not supplied-    Just ts -> maybe (raise err) (return . Just) (parseParam net ctx ts)-  where-    label = proxyLabel (Proxy :: Proxy a)-    err = UserError $ "Unable to parse param " <> T.unpack label---- | Raises an exception if the parameter is not supplied-paramRequired :: forall a m. (Param a, MonadUnliftIO m) => ActionT m a-paramRequired = maybe (raise err) return =<< paramOptional-  where-    label = T.unpack (proxyLabel (Proxy :: Proxy a))-    err = UserError $ "Param " <> label <> " not defined"---- | Returns the default value of a parameter if it is not supplied. Raises an--- exception on parse failure.-paramDef :: (Default a, Param a, MonadUnliftIO m) => ActionT m a-paramDef = fromMaybe def <$> paramOptional---- | Does not raise exceptions. Will call @Scotty.next@ if the parameter is--- not supplied or if parsing fails.-paramCapture :: forall a m. (Param a, MonadUnliftIO m) => ActionT m a-paramCapture = do-  net <- askl (.config.store.net)-  ctx <- askl (.config.store.ctx)-  p <- S.captureParam label `rescue` S.next-  maybe S.next return (parseParam net ctx p)-  where-    label = proxyLabel (Proxy :: Proxy a)---- | Read the transaction that a request carries in its body.-parseTxBody :: (MonadUnliftIO m) => ActionT m Tx-parseTxBody = do-  net <- askl (.config.store.net)-  b <- L.toStrict <$> S.body-  case decodeTxBody net b of-    Left _ -> raise $ UserError "Failed to parse request body"-    Right tx -> return tx---- | Read a transaction from a request body. The body is the transaction in--- hex or as raw bytes. White space in the hex is ignored.------ The body must hold one transaction and nothing else. Bytes after the--- transaction are refused, as Bitcoin Core does for a raw transaction.------ The transaction must be in the format that peers of the network read. A--- network without SegWit, such as Bitcoin Cash, has the format without--- witness only. Its nodes would not read the SegWit format, so the store--- does not take it either.-decodeTxBody :: Network -> C.ByteString -> Either String Tx-decodeTxBody net body = fromHex <> fromBytes body-  where-    fromBytes = runGetS $ do-      tx <- getNetworkTx net-      done <- isEmpty-      unless done $ fail "Trailing bytes after transaction"-      return tx-    fromHex =-      case parseBase16 (C.filter (not . isSpace) body) of-        Right b16 -> fromBytes (decodeBase16 b16)-        Left e -> Left (cs e)---- | Offset in the @offset@ parameter. Responds with an error if above the maximum.-parseOffset :: (MonadUnliftIO m) => ActionT m OffsetParam-parseOffset = do-  res@(OffsetParam o) <- paramDef-  limits <- askl (.config.limits)-  when (limits.maxOffset > 0 && fromIntegral o > limits.maxOffset) $-    raise . UserError $-      "offset exceeded: " <> show o <> " > " <> show limits.maxOffset-  return res---- | Turn a start parameter into a position: a transaction, the block of a block--- hash, a height, or the last block at or before a time.-parseStart ::-  (MonadUnliftIO m) =>-  Maybe StartParam ->-  ActionT m (Maybe Start)-parseStart Nothing = return Nothing-parseStart (Just s) =-  runMaybeT $-    case s of-      StartParamHash {hash = h} -> start_tx h <|> start_block h-      StartParamHeight {height = h} -> start_height h-      StartParamTime {time = q} -> start_time q-  where-    start_height h = return $ AtBlock $ fromIntegral h-    start_block h = do-      b <- MaybeT $ getBlock (H.BlockHash h)-      return $ AtBlock b.height-    start_tx h = do-      _ <- MaybeT $ getTxData (TxHash h)-      return $ AtTx (TxHash h)-    start_time q = do-      ch <- lift $ askl (.config.store.chain)-      b <- MaybeT $ blockAtOrBefore ch q-      return $ AtBlock b.height---- | Limit, offset and start in the parameters of a request.-parseLimits :: (MonadUnliftIO m) => ActionT m LimitsParam-parseLimits = LimitsParam <$> paramOptional <*> parseOffset <*> paramOptional---- | Pagination of a query from request parameters, within the limits for full or short items.-paramToLimits ::-  (MonadUnliftIO m) =>-  Bool ->-  LimitsParam ->-  ActionT m Limits-paramToLimits full (LimitsParam limitM o startM) = do-  wl <- askl (.config.limits)-  Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart startM---- | Limit of a query: the requested one or the default, but no more than the maximum.-validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32-validateLimit wl full limitM =-  f m $ maybe d (fromIntegral . (.get)) limitM-  where-    m-      | full && wl.maxFullItemCount > 0 = wl.maxFullItemCount-      | otherwise = wl.maxItemCount-    d = wl.defItemCount-    f a 0 = a-    f 0 b = b-    f a b = min a b-------------------- Utilities ---------------------- | Run a query against the database, through the cache if there is one.-runInWebReader ::-  (MonadIO m) =>-  CacheT (DatabaseReaderT m) a ->-  ReaderT WebState m a-runInWebReader f = do-  bdb <- asks (.config.store.db)-  mc <- asks (.config.store.cache)-  lift $ runReaderT (withCache mc f) bdb---- | Run queries without the cache if the flag is 'True'.-runNoCache ::-  (MonadIO m) =>-  Bool ->-  ReaderT WebState m a ->-  ReaderT WebState m a-runNoCache False = id-runNoCache True = local $ \s ->-  s {config = s.config {store = s.config.store {cache = Nothing}}}---- | Middleware that logs every request with its response status: errors at error level, others at debug level.-logIt :: Logger -> Middleware-logIt logger app req respond =-  app req $ \res -> do-    let s = responseStatus res-        msg = fmtReq req <> ": " <> fmtStatus s-    if statusIsSuccessful s-      then logDebug logger msg-      else logError logger msg-    respond res---- | Describe a request for logs.-fmtReq :: Request -> Text-fmtReq req =-  let m = requestMethod req-      v = httpVersion req-      p = rawPathInfo req-      q = rawQueryString req-   in T.decodeUtf8 (m <> " " <> p <> q <> " " <> cs (show v))---- | Describe a response status for logs.-fmtStatus :: Status -> Text-fmtStatus s = cs (show (statusCode s)) <> " " <> cs (statusMessage s)---- | Run a handler, or another if the first one fails.-rescue ::-  (MonadUnliftIO m) =>-  S.ActionT m a ->-  S.ActionT m a ->-  S.ActionT m a-x `rescue` y = x `S.catch` \(_ :: S.ScottyException) -> y---- | Read a request parameter from the path, query string or form.-param ::-  (MonadUnliftIO m, S.Parsable a) =>-  Text ->-  S.ActionT m a-param t = S.queryParam t `rescue` S.formParam t---- | Read a field of the environment from a monad transformer.-askl :: (MonadTrans t, MonadReader r m) => (r -> a) -> t m a-askl f = lift (asks f)+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -Wno-deprecations #-}++-- |+-- Module      : Haskoin.Store.Web+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- REST API server of Haskoin Store, running directly on WAI and Warp.+--+-- 'runWeb' serves the endpoints described in "Haskoin.Store.WebCommon" and a+-- set of endpoints compatible with the Blockchain.info API under+-- @\/blockchain\/@. Responses are JSON by default, pretty-printed with+-- @?pretty=true@, or binary if the request has the header+-- @Accept: application\/octet-stream@. New blocks and mempool transactions are+-- streamed at @\/events@, as lines of JSON or over a WebSocket.+--+-- Limits on the size of responses are set with 'WebLimits'. Errors are+-- returned as JSON with an HTTP status that depends on the 'Except'.+-- = Design of the API+--+-- * __Two encodings of the same data.__ JSON is easy to use from anywhere. The+--   binary encoding is several times smaller and faster to produce and parse, and+--   is what the Haskell client uses.+--+-- * __Limits protect the server.__ A single request for every transaction of a+--   busy address could take minutes and gigabytes. 'WebLimits' caps list sizes+--   and offsets, and lower caps apply to full transactions, which are much larger+--   than references.+--+-- * __Health is measured, not assumed.__ The store reports itself unhealthy when+--   it lags behind the network, stops receiving blocks or transactions, has too+--   few peers or a backlog of transactions. Load balancers can route around an+--   instance that is still syncing or has lost its peers.+--+-- * __Slow event clients are dropped.__ Each @\/events@ client gets a bounded+--   queue. A client that falls too far behind is disconnected rather than allowed+--   to make the server buffer events without limit.+--+-- * __Compatibility endpoints ease migration.__ The @\/blockchain\/@ endpoints+--   follow the Blockchain.info API so that existing wallets can switch to a+--   self-hosted server with few changes.+--+-- * __Handlers are plain functions.__ A handler takes the 'WebState' and+--   the request ('Req'), and returns a response in 'IO'. The state holds the+--   queries to answer with in 'query' ('webQuery': the database, with the+--   cache in front of the queries about xpubs); @nocache=true@ is the same+--   handler given a state whose queries read only the database ('cacheOr').+--   The state is made for each request, around one view of the database,+--   so that everything a request reads is of one moment; a store that is a+--   member of a fleet takes the view at the block that its fleet answers+--   from, with the mempool that the fleet shares on top, and has the+--   fleet's cache of xpubs in front of the queries about them+--   ("Haskoin.Store.Fleet.Member").+--   Routes, parameters and the passing of a request to the next route are+--   in "Haskoin.Store.Web.Router", a few dozen lines over WAI. A handler+--   stops early by throwing an 'Except', which becomes the response+--   ('guarded'). There is no web framework and no monad transformer: a+--   framework runs every handler in a stack of its own, and a reader monad+--   in it would hide the state.+module Haskoin.Store.Web+  ( -- * Web+    WebConfig (..),+    Except (..),+    WebLimits (..),+    runWeb,+    runWebSocket,++    -- * Request parsing+    decodeTxBody,+    acceptsBinary,+  )+where++import Conduit+  ( ConduitT,+    await,+    concatMapC,+    concatMapMC,+    dropC,+    dropWhileC,+    headC,+    lengthC,+    mapC,+    runConduit,+    sinkList,+    takeC,+    takeWhileC,+    yield,+    (.|),+  )+import Control.Applicative ((<|>))+import Control.Lens ((.~), (^.))+import Control.Monad+  ( filterM,+    forM_,+    forever,+    guard,+    join,+    unless,+    void,+    when,+    (<=<),+  )+import Control.Monad.Trans (lift)+import Data.Aeson+  ( Encoding,+    ToJSON (..),+    Value,+  )+import Data.Aeson qualified as A+import Data.Aeson.Encode.Pretty+  ( Config (..),+    defConfig,+    encodePretty',+  )+import Data.Aeson.Encoding+  ( encodingToLazyByteString,+    list,+  )+import Data.Aeson.Types (parseMaybe)+import Data.ByteString.Base16 (decodeBase16, parseBase16)+import Data.ByteString.Builder (lazyByteString)+import Data.ByteString.Char8 qualified as C+import Data.ByteString.Lazy qualified as L+import Data.Bytes.Get+import Data.Bytes.Put+import Data.Bytes.Serial+import Data.Char (isSpace)+import Data.Default (Default (..))+import Data.Function ((&))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.Int (Int64)+import Data.List (nub)+import Data.Maybe+  ( catMaybes,+    fromJust,+    fromMaybe,+    isJust,+    listToMaybe,+    mapMaybe,+    maybeToList,+  )+import Data.Proxy (Proxy (..))+import Data.Serialize (Serialize, decode, encode)+import Data.String (fromString)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Encoding qualified as TL+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word32, Word64)+import Database.RocksDB+  ( Property (..),+    getProperty,+  )+import Haskoin.Address+import Haskoin.Block qualified as H+import Haskoin.Crypto.Keys+import Haskoin.Network+import Haskoin.Node+  ( Chain,+    Logger,+    OnlinePeer (..),+    PeerMgr,+    chainGetAncestor,+    chainGetBest,+    getPeers,+    logDebug,+    logError,+    sendMessage,+    withSource,+  )+import Haskoin.Script+import Haskoin.Store.BlockStore+import Haskoin.Store.Cache+import Haskoin.Store.Common hiding+  ( blockAtOrAfter,+    blockAtOrAfterMTP,+    blockAtOrBefore,+    getActiveBlock,+    getActiveTxData,+    getDefaultBalance,+    getNumTransaction,+    getTransaction,+    getTransactions,+  )+import Haskoin.Store.Common qualified as C+import Haskoin.Store.Data+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader+import Haskoin.Store.Fleet.Member+  ( Member (..),+    TipMoved (..),+    answeringCached,+    memberAlive,+    memberEvents,+    memberTip,+  )+import Haskoin.Store.Fleet.Redis (FleetEvent (..))+import Haskoin.Store.Manager+import Haskoin.Store.Web.Docs (swaggerPage, swaggerYaml)+import Haskoin.Store.Web.Events+import Haskoin.Store.Web.Router hiding (header)+import Haskoin.Store.Web.Router qualified as Router+import Haskoin.Store.WebCommon+import Haskoin.Transaction+import Haskoin.Util+import NQE+  ( subscribe,+    unsubscribe,+  )+import Network.HTTP.Types+  ( Status (..),+    hContentType,+    methodGet,+    methodPost,+    parseQuery,+    renderStdMethod,+    status200,+    status203,+    status204,+    status400,+    status404,+    status409,+    status413,+    status500,+    status503,+    statusIsClientError,+    statusIsServerError,+    statusIsSuccessful,+  )+import Network.Socket (Socket)+import Network.Wai+  ( Application,+    Middleware,+    Request (..),+    Response,+    mapResponseHeaders,+    modifyResponse,+    responseLBS,+    responseStatus,+    responseStream,+  )+import Network.Wai.Handler.Warp+  ( defaultSettings,+    runSettings,+    runSettingsSocket,+    setHost,+    setPort,+  )+import Network.Wai.Handler.WarpTLS (runTLS, runTLSSocket, tlsSettings)+import Network.Wai.Handler.WebSockets (websocketsOr)+import Network.Wai.Middleware.Autohead (autohead)+import Network.Wai.Middleware.Gzip (gzip)+import Network.Wai.Middleware.Gzip qualified as Gzip+import Network.WebSockets+  ( acceptRequest,+    defaultConnectionOptions,+    pendingRequest,+    rejectRequestWith,+    requestPath,+    sendBinaryData,+    sendTextData,+  )+import Network.WebSockets qualified as WebSockets+import Network.WebSockets.Connection.PingPong+  ( PingPongOptions (..),+    defaultPingPongOptions,+    withPingPong,+  )+import Network.Wreq qualified as Wreq+import Network.Wreq.Session as Wreq (Session)+import Network.Wreq.Session qualified as Wreq.Session+import Numeric.Natural (Natural)+import System.Metrics.StatsD+import Text.Read (readMaybe)+import UnliftIO+  ( TVar,+    async,+    atomically,+    bracket,+    bracket_,+    catch,+    catchAny,+    fromException,+    handleAny,+    link,+    newIORef,+    newTQueueIO,+    newTVarIO,+    race_,+    readIORef,+    readTQueue,+    readTVarIO,+    stateTVar,+    throwIO,+    timeout,+    try,+    withAsync,+    writeIORef,+    writeTQueue,+    writeTVar,+  )+import UnliftIO.Concurrent (threadDelay)++-- | Limits of the API. A value of @0@ means no limit.+data WebLimits = WebLimits+  { -- | maximum number of items in a list response+    maxItemCount :: !Word32,+    -- | maximum number of full transactions in a list response+    maxFullItemCount :: !Word32,+    -- | maximum offset of a paged query+    maxOffset :: !Word32,+    -- | number of items in a list response when no limit is given+    defItemCount :: !Word32,+    -- | consecutive unused addresses after which xpub derivation stops+    xpubGap :: !Word32,+    -- | unused addresses to look at for an xpub with no transactions+    xpubGapInit :: !Word32,+    -- | seconds without a new transaction before the store is reported+    -- unhealthy+    txTimeout :: !Word64,+    -- | seconds without a new block before the store is reported unhealthy+    blockTimeout :: !Word64+  }+  deriving (Eq, Show)++-- | 200000 items, 5000 full transactions, offset up to 50000, 100 items by+-- default, xpub gaps of 32 and 20, transaction timeout of 30 minutes and+-- block timeout of four hours.+instance Default WebLimits where+  def =+    WebLimits+      { maxItemCount = 200000,+        maxFullItemCount = 5000,+        maxOffset = 50000,+        defItemCount = 100,+        xpubGap = 32,+        xpubGapInit = 20,+        txTimeout = 3600 `div` 2,+        blockTimeout = 4 * 3600+      }++-- | Configuration of the web server.+data WebConfig = WebConfig+  { -- | address to listen on, such as @*@ for all+    host :: !String,+    -- | port to listen on+    port :: !Int,+    -- | running store to serve+    store :: !Store,+    -- | blocks that the database may lag behind the headers before the store+    -- is reported unhealthy+    maxLaggingBlocks :: !Int,+    -- | transactions that may be waiting to be imported before the store is+    -- reported unhealthy+    maxPendingTxs :: !Int,+    -- | fewest peers before the store is reported unhealthy+    minPeers :: !Int,+    -- | limits of the API+    limits :: !WebLimits,+    -- | version reported by the health check+    version :: !String,+    -- | the mempool is not indexed, so do not report it in health checks+    noMempool :: !Bool,+    -- | StatsD client, if metrics are enabled+    stats :: !(Maybe Stats),+    -- | microseconds between updates of the price ticker+    tickerRefresh :: !Int,+    -- | URL of the price ticker service+    tickerURL :: !String,+    -- | URL of the historical price service+    priceHistoryURL :: !String,+    -- | disable the xpub endpoints+    noXPub :: !Bool,+    -- | disable the Blockchain.info compatible endpoints+    noBlockchainInfo :: !Bool,+    -- | seconds between health checks+    healthCheckInterval :: !Int,+    -- | a certificate and its private key, in files in PEM format, to+    -- serve HTTPS instead of HTTP; 'Nothing' for HTTP, which is the+    -- default+    tls :: !(Maybe (FilePath, FilePath)),+    -- | log messages here+    logger :: !Logger+  }++-- | State shared by all request handlers.+data WebState = WebState+  { -- | configuration+    config :: !WebConfig,+    -- | latest prices by currency code+    ticker :: !(TVar (HashMap Text BinfoTicker)),+    -- | StatsD metrics, if enabled+    metrics :: !(Maybe WebMetrics),+    -- | HTTP session for price requests+    session :: !Wreq.Session,+    -- | result of the latest health check+    health :: !(TVar HealthCheck),+    -- | the queries to answer with, the cache included ('webQuery'): those+    -- of one view of the database, taken for the request+    query :: !StoreRead,+    -- | the same queries without the cache, on the same view+    direct :: !StoreRead,+    -- | whether Redis answered the last time it was asked; always 'True'+    -- for a store without a cache+    cacheUp :: !(TVar Bool),+    -- | the events that the clients of @\/events@ read ('feedEvents')+    eventLog :: !EventLog+  }++-- | StatsD metrics of the web server: a timing for each kind of request,+-- under @web.\<name\>@, error counters and a gauge of connected event+-- clients.+data WebMetrics = WebMetrics+  { -- Addresses+    addressTx :: !StatTiming,+    addressTxFull :: !StatTiming,+    addressBalance :: !StatTiming,+    addressUnspent :: !StatTiming,+    xpub :: !StatTiming,+    xpubDelete :: !StatTiming,+    xpubTxFull :: !StatTiming,+    xpubTx :: !StatTiming,+    xpubBalance :: !StatTiming,+    xpubUnspent :: !StatTiming,+    -- Transactions+    tx :: !StatTiming,+    txRaw :: !StatTiming,+    txAfter :: !StatTiming,+    txBlock :: !StatTiming,+    txBlockRaw :: !StatTiming,+    txPost :: !StatTiming,+    mempool :: !StatTiming,+    -- Blocks+    block :: !StatTiming,+    blockRaw :: !StatTiming,+    -- Blockchain+    binfoMultiaddr :: !StatTiming,+    binfoBalance :: !StatTiming,+    binfoAddressRaw :: !StatTiming,+    binfoUnspent :: !StatTiming,+    binfoTxRaw :: !StatTiming,+    binfoBlock :: !StatTiming,+    binfoBlockHeight :: !StatTiming,+    binfoBlockLatest :: !StatTiming,+    binfoBlockRaw :: !StatTiming,+    binfoMempool :: !StatTiming,+    binfoExportHistory :: !StatTiming,+    -- Blockchain /q endpoints+    binfoQaddresstohash :: !StatTiming,+    binfoQhashtoaddress :: !StatTiming,+    binfoQaddrpubkey :: !StatTiming,+    binfoQpubkeyaddr :: !StatTiming,+    binfoQhashpubkey :: !StatTiming,+    binfoQgetblockcount :: !StatTiming,+    binfoQlatesthash :: !StatTiming,+    binfoQbcperblock :: !StatTiming,+    binfoQtxtotalbtcoutput :: !StatTiming,+    binfoQtxtotalbtcinput :: !StatTiming,+    binfoQtxfee :: !StatTiming,+    binfoQtxresult :: !StatTiming,+    binfoQgetreceivedbyaddress :: !StatTiming,+    binfoQgetsentbyaddress :: !StatTiming,+    binfoQaddressbalance :: !StatTiming,+    binfoQaddressfirstseen :: !StatTiming,+    -- Errors+    serverErrors :: StatCounter,+    clientErrors :: StatCounter,+    -- Others+    health :: !StatTiming,+    peers :: !StatTiming,+    db :: !StatTiming,+    events :: !StatGauge+  }++-- | Register the web server metrics with a StatsD client.+createMetrics :: Stats -> IO WebMetrics+createMetrics s = do+  -- Addresses+  addressTx <- d "address_transactions"+  addressTxFull <- d "address_transactions_full"+  addressBalance <- d "address_balance"+  addressUnspent <- d "address_unspent"+  xpub <- d "xpub"+  xpubDelete <- d "xpub_delete"+  xpubTxFull <- d "xpub_transactions_full"+  xpubTx <- d "xpub_transactions"+  xpubBalance <- d "xpub_balances"+  xpubUnspent <- d "xpub_unspent"++  -- Transactions+  tx <- d "transaction"+  txRaw <- d "transaction_raw"+  txAfter <- d "transaction_after"+  txPost <- d "transaction_post"+  txBlock <- d "transactions_block"+  txBlockRaw <- d "transactions_block_raw"+  mempool <- d "mempool"++  -- Blocks+  block <- d "block"+  blockRaw <- d "block_raw"++  -- Blockchain+  binfoMultiaddr <- d "blockchain_multiaddr"+  binfoBalance <- d "blockchain_balance"+  binfoAddressRaw <- d "blockchain_rawaddr"+  binfoUnspent <- d "blockchain_unspent"+  binfoTxRaw <- d "blockchain_rawtx"+  binfoBlock <- d "blockchain_blocks"+  binfoBlockHeight <- d "blockchain_block_height"+  binfoBlockLatest <- d "blockchain_latestblock"+  binfoBlockRaw <- d "blockchain_rawblock"+  binfoMempool <- d "blockchain_mempool"+  binfoExportHistory <- d "blockchain_export_history"++  -- Blockchain /q endpoints+  binfoQaddresstohash <- d "blockchain_q_addresstohash"+  binfoQhashtoaddress <- d "blockchain_q_hashtoaddress"+  binfoQaddrpubkey <- d "blockckhain_q_addrpubkey"+  binfoQpubkeyaddr <- d "blockchain_q_pubkeyaddr"+  binfoQhashpubkey <- d "blockchain_q_hashpubkey"+  binfoQgetblockcount <- d "blockchain_q_getblockcount"+  binfoQlatesthash <- d "blockchain_q_latesthash"+  binfoQbcperblock <- d "blockchain_q_bcperblock"+  binfoQtxtotalbtcoutput <- d "blockchain_q_txtotalbtcoutput"+  binfoQtxtotalbtcinput <- d "blockchain_q_txtotalbtcinput"+  binfoQtxfee <- d "blockchain_q_txfee"+  binfoQtxresult <- d "blockchain_q_txresult"+  binfoQgetreceivedbyaddress <- d "blockchain_q_getreceivedbyaddress"+  binfoQgetsentbyaddress <- d "blockchain_q_getsentbyaddress"+  binfoQaddressbalance <- d "blockchain_q_addressbalance"+  binfoQaddressfirstseen <- d "blockchain_q_addressfirstseen"++  -- Errors+  serverErrors <- c "server_errors"+  clientErrors <- c "client_errors"++  -- Others+  health <- d "health"+  peers <- d "peers"+  db <- d "dbstats"++  events <- g "events_connected"+  return WebMetrics {..}+  where+    d x = newStatTiming s ("web." <> x) 10+    g x = newStatGauge s ("web." <> x) 0+    c x = newStatCounter s ("web." <> x) 10++-- | Increase a gauge while an action runs.+withGaugeIO :: StatGauge -> IO a -> IO a+withGaugeIO g = bracket_ (incrementGauge g 1) (decrementGauge g 1)++-- | Increase a gauge of the web metrics while a handler runs.+withGaugeIncrease ::+  WebState ->+  (WebMetrics -> StatGauge) ->+  IO a ->+  IO a+withGaugeIncrease st gf go =+  case st.metrics of+    Nothing -> go+    Just m -> withGaugeIO (gf m) go++-- | Record how long a handler takes in a timing of the web metrics.+withMetrics ::+  WebState ->+  (WebMetrics -> StatTiming) ->+  IO a ->+  IO a+withMetrics st df go =+  case st.metrics of+    Nothing -> go+    Just m -> bracket time (stop m) (const go)+  where+    time = round . (* 1000) <$> getPOSIXTime+    stop m t1 = do+      t2 <- time+      addTiming (df m) (t2 - t1)++-- | Encoding of a response.+data SerialAs+  = -- | binary, for @Accept: application\/octet-stream@+    SerialAsBinary+  | -- | compact JSON+    SerialAsJSON+  | -- | indented JSON, for @?pretty=true@+    SerialAsPrettyJSON+  deriving (Eq, Show)++-------------------+-- Path Handlers --+-------------------++-- | Run the web server forever on the host and port of the configuration.+-- Also starts a thread that updates prices, unless the Blockchain.info+-- endpoints are disabled, and a thread that runs health checks.+--+-- = HTTPS+--+-- With a certificate and a key in 'tls', the server speaks HTTPS on that+-- port, and nothing else: TLS 1.2 and 1.3, and HTTP\/2 for clients that+-- ask for it. It is off by default. A public service usually sits behind+-- a proxy that has the certificate, limits requests and spreads them over+-- servers; this is for a server by itself, and for links between machines+-- that should not be in the clear. The files are read when the server+-- starts, so a renewed certificate takes a restart.+runWeb :: WebConfig -> IO ()+runWeb config = serveWeb config $ case config.tls of+  Nothing -> runSettings settings+  Just (certificate, key) -> runTLS (tlsSettings certificate key) settings+  where+    settings = setPort config.port . setHost (fromString config.host) $ defaultSettings++-- | Like 'runWeb', on a socket that already listens; the host and port of the+-- configuration are ignored. Binding port 0 and passing the socket gives a+-- free port with no window in which another program can take it, which+-- finding a free port first and binding it later has.+runWebSocket :: WebConfig -> Socket -> IO ()+runWebSocket config sock = serveWeb config $ case config.tls of+  Nothing -> runSettingsSocket defaultSettings sock+  Just (certificate, key) -> runTLSSocket (tlsSettings certificate key) defaultSettings sock++-- | Run the web server with a function of Warp that serves an application.+-- The application is the router ("Haskoin.Store.Web.Router") with the+-- routes of 'handlePaths', behind the middleware for WebSocket events, for+-- logs and for the header that lets any origin make requests.+serveWeb :: WebConfig -> (Application -> IO ()) -> IO ()+serveWeb config serve = do+  ticker <- newTVarIO HashMap.empty+  metrics <- mapM createMetrics config.stats+  session <- Wreq.Session.newAPISession+  health <- runHealthCheck >>= newTVarIO+  cacheUp <- cacheCheck >>= newTVarIO+  eventLog <- newEventLog+  let db = config.store.db+      stateOf q = WebState {query = webQuery config.store cacheUp q, direct = q, ..}+      routesOf s = map (guarded s) (concatMap withPost (handlePaths s config))+      -- outside of a request, each query has a view of its own; a request+      -- has one view for all that it reads, which is let go when its+      -- response is sent+      st = stateOf (L.standaloneRead db)+      local answer = L.withView db (answer . routesOf . stateOf . L.layeredRead)+      scoped answer = case config.store.fleet of+        Nothing -> local answer+        Just member ->+          readTVarIO cacheUp >>= \case+            True -> shared member (3 :: Int) False answer+            -- without Redis there is no fleet to answer with+            False -> local answer+      -- as a member of a fleet: from the view at the fleet tip, with the+      -- pool that the fleet shares on top. The tip is taken to be the one+      -- that the member read at its last turn, which saves asking Redis+      -- for it: the first read of the pool checks it, and a request that+      -- reads nothing from the pool checks it when its handler is done. If+      -- it is not the tip any more, the request is answered again, from the+      -- tip that Redis has.+      shared member tries fresh answer = do+        known <- if fresh then return Nothing else fmap encode <$> readTVarIO member.tip+        asked <- case known of+          Just tip -> return (Just (Just tip))+          Nothing -> timeout 2000000 (memberTip member) `catchAny` \_ -> return Nothing+        case asked of+          Nothing -> atomically (writeTVar cacheUp False) >> local answer+          -- no store has reported yet+          Just Nothing -> local answer+          Just (Just tip) -> do+            handled <- newIORef False+            let marked confirm r = r {handler = \req -> r.handler req <* confirm <* writeIORef handled True} :: Route+                -- extended public keys through the fleet's cache of+                -- them, unless a request asks for the database+                inFleet plain cached confirm =+                  answer (map (marked confirm) (routesOf WebState {query = cached, direct = plain, ..}))+            try (answeringCached member tip inFleet) >>= \case+              Right (Just sent) -> return sent+              Right Nothing+                -- a tip from the member's last turn of which there is no+                -- view may not be the tip any more+                | not fresh && tries > 0 -> shared member (tries - 1) True answer+                | otherwise -> behind answer+              Left TipMoved ->+                -- the tip moved while the handler was reading: answer+                -- again from the new one, unless the response is on its way+                readIORef handled >>= \case+                  True -> throwIO TipMoved+                  False+                    | tries > 0 -> shared member (tries - 1) True answer+                    | otherwise -> behind answer+      behind answer = answer [r {handler = behindResponse st} :: Route | r <- routes]+      -- the stream of events never ends, and reads nothing from the+      -- database: answered like the rest it would hold a view, and the+      -- data under it, for as long as its client listens+      app request+        | pathInfo request == ["events"] = routerScoped routes ($ routes) notFound request+        | otherwise = routerScoped routes scoped notFound request+      routes = routesOf st+      notFound req NoRoute = errorResponse st req ThingNotFound+      notFound req (Unknown ks) =+        errorResponse st req . UserError $+          "Unknown parameter: " <> T.unpack (T.intercalate ", " ks)+  withAsync (priceUpdater session ticker) $ \_a1 ->+    withAsync (healthCheckLoop health cacheUp) $ \_a2 ->+      withAsync (feedEvents st) $ \a3 ->+        link a3 >> do+          serve . webSocketEvents st . logIt config.logger . anyOrigin . preflight . autohead . compressed $ app+  where+    priceUpdater session =+      unless config.noBlockchainInfo+        . price+          (withSource "Price" config.logger)+          config.store.net+          session+          config.tickerURL+          config.tickerRefresh+    runHealthCheck = healthCheck config+    cacheCheck = case (config.store.cache, config.store.fleet) of+      (Just cache, _) -> cacheAlive cache+      (_, Just member) -> memberAlive member+      _ -> return True+    healthCheckLoop v up = loopForever $ do+      threadDelay (config.healthCheckInterval * 1000 * 1000)+      h <- runHealthCheck+      atomically (writeTVar v h)+      -- a store whose checks fail stops taking its turns as a member of+      -- its fleet, so that it does not hold the others back+      forM_ config.store.fleet $ \member -> atomically (writeTVar member.healthy (isOK h))+      cacheCheck >>= atomically . writeTVar up+    -- what a browser asks before a request from another site that is not+    -- of the plainest kind: which methods and headers it may use. Without+    -- an answer it does not send the request.+    preflight inner req respond+      | requestMethod req == "OPTIONS" =+          respond $+            responseLBS+              status204+              [ ("Allow", allowed),+                ("Access-Control-Allow-Methods", allowed),+                ("Access-Control-Allow-Headers", "Accept, Content-Type"),+                ("Access-Control-Max-Age", "86400")+              ]+              ""+      | otherwise = inner req respond+    allowed = "GET, HEAD, POST, DELETE, OPTIONS"+    -- answers are compressed for a client that says it reads gzip: JSON+    -- shrinks several times over, and the binary format, which is mostly+    -- hashes, by less. The events are a stream that has to leave as it is+    -- written, and are left alone.+    compressed inner req+      | pathInfo req == ["events"] = inner req+      | otherwise = gzip gzipSettings inner req+    gzipSettings =+      Gzip.defaultGzipSettings+        { Gzip.gzipCheckMime = \m -> Gzip.defaultCheckMime m || "application/octet-stream" `C.isPrefixOf` m+        }+    -- any origin may ask, and what is answered depends on the format that+    -- was asked for, which a cache between the client and the server has+    -- to know: it must not give the JSON of one client to another that+    -- asked for binary. The same goes for whether it is compressed, and+    -- the two are said in one header.+    anyOrigin = modifyResponse (mapResponseHeaders varied)+    varied headers =+      [("Access-Control-Allow-Origin", "*"), ("Vary", "Accept, Accept-Encoding")]+        <> filter ((/= "Vary") . fst) headers++-- | Answer the errors of the handler of a route: an 'Except' with its+-- status and JSON, any other error as a server error, after logging it. A+-- request passed on to the next route goes on.+guarded :: WebState -> Route -> Route+guarded st r = r {handler = \req -> r.handler req `catchAny` answer req}+  where+    answer req e+      | Just Next <- fromException e = throwIO Next+      | Just TipMoved <- fromException e = throwIO TipMoved+      | Just x <- fromException e = errorResponse st req x+      | otherwise = do+          logError st.config.logger $ fmtReq req.wai <> ": " <> cs (show e)+          errorResponse st req ServerError++-- | What a store that is a member of a fleet answers when it has no view of+-- the block that the fleet answers from: 503, so that a load balancer sends+-- the request to another store. Any answer of its own could differ from+-- those of the other stores, which is what a fleet is there to prevent.+behindResponse :: WebState -> Req -> IO Response+behindResponse st req = errorResponse st req BehindFleet++-- | The response to an error, counted in the metrics, in the format that+-- the request asked for.+--+-- Errors were JSON whatever was asked, because 'Except' had no binary+-- encoding. A client that speaks binary had to carry a JSON decoder for+-- the one answer that it least expects. From haskoin-store-data 1.10 an+-- error has a binary encoding like everything else.+errorResponse :: WebState -> Req -> Except -> IO Response+errorResponse st req e = do+  forM_ st.metrics $ \m ->+    mapM_ (`incrementCounter` 1) $+      if+        | statusIsClientError (errStatus e) -> Just m.clientErrors+        | statusIsServerError (errStatus e) -> Just m.serverErrors+        | otherwise -> Nothing+  return $+    if wantsBinary req+      then responseLBS (errStatus e) ((hContentType, "application/octet-stream") : again) (runPutL (serialize e))+      else responseLBS (errStatus e) ((hContentType, "application/json") : again) (A.encode e)+  where+    -- a server behind its fleet catches up within a block or is taken out+    -- of rotation: a client or a load balancer that reads this header+    -- tries again in a second, and elsewhere meanwhile+    again = [("Retry-After", "1") | e == BehindFleet]++-- | Historical prices of the coin of the network in a currency at several times. Returns an empty list on failure.+getRates ::+  Logger ->+  Network ->+  Wreq.Session ->+  String ->+  Text ->+  [Word64] ->+  IO [BinfoRate]+getRates logger net session url currency times = do+  handleAny err $ do+    r <-+      Wreq.asJSON+        =<< Wreq.Session.postWith opts session url body+    return $ r ^. Wreq.responseBody+  where+    err _ = do+      logError logger "Could not get historic prices"+      return []+    body = toJSON times+    base =+      Wreq.defaults+        & Wreq.param "base" .~ [T.toUpper $ T.pack net.name]+    opts = base & Wreq.param "quote" .~ [currency]++-- | Update the prices of the coin of the network periodically, for BTC and BCH only.+price ::+  Logger ->+  Network ->+  Wreq.Session ->+  String ->+  Int ->+  TVar (HashMap Text BinfoTicker) ->+  IO ()+price logger net session url pget v = forM_ purl $ \u -> loopForever $ do+  let err e = logError logger $ cs (show e)+  handleAny err $ do+    r <- Wreq.asJSON =<< Wreq.Session.get session u+    atomically . writeTVar v $ r ^. Wreq.responseBody+  threadDelay pget+  where+    purl = case code of+      Nothing -> Nothing+      Just x -> Just (url <> "?base=" <> x)+      where+        code+          | net == btc = Just "btc"+          | net == bch = Just "bch"+          | otherwise = Nothing++-- | Stop handling the request, and answer with an error ('guarded').+raise :: WebState -> Except -> IO a+raise _ = throwIO++-- | HTTP status of an error.+errStatus :: Except -> Status+errStatus ThingNotFound = status404+errStatus BadRequest = status400+errStatus UserError {} = status400+errStatus StringError {} = status400+errStatus ServerError = status500+errStatus TxIndexConflict {} = status409+errStatus ServerTimeout = status500+errStatus RequestTooLarge = status413+errStatus BehindFleet = status503++-- | A route, and with it the same route under @POST@ if it takes a list in+-- a parameter: addresses, blocks, heights or transactions.+--+-- A list in the query string has to fit in a URL, and proxies cut those+-- off at a few thousand characters: some hundred addresses. The router+-- reads the parameters of a form in the body as it reads those of the+-- query string, so under @POST@ the same parameters go in the body, with+-- @Content-Type: application\/x-www-form-urlencoded@, and have no such+-- limit. @\/transactions@ is left out: @POST@ there publishes a+-- transaction.+withPost :: Route -> [Route]+withPost r+  | r.method == methodGet && takesList && r.pattern /= [Left "transactions"] = [r, r {method = methodPost}]+  | otherwise = [r]+  where+    takesList = any (`elem` r.accepts) ["addresses", "blocks", "heights", "txids"]++-- | The parameters of @\/events@ that say what a client hears about from+-- the start ('eventsOpen').+eventParams :: [Text]+eventParams = ["quiet", "addresses", "xpub", "derive", "filter", "functions", "tweak", "flags"]++-- | The routes of every endpoint, except those disabled in the configuration.+handlePaths :: WebState -> WebConfig -> [Route]+handlePaths st cfg =+  [ marshalled ["notx"] (\r -> GetBlockBest <$> dflt r) (handleBlockBest st),+    marshalledList ["notx"] (\r -> GetBlockHeight <$> cap r <*> dflt r) (handleBlockHeight st),+    marshalled ["notx"] (\r -> GetBlockTime <$> cap r <*> dflt r) (handleBlockTime st),+    marshalled ["notx"] (\r -> GetBlockMTP <$> cap r <*> dflt r) (handleBlockMTP st),+    marshalled ["notx"] (\r -> GetBlock <$> cap r <*> dflt r) (handleBlock st),+    marshalled [] (\r -> GetTx <$> cap r) (handleTx st),+    plain [] (\r -> GetTxRaw <$> cap r) (handleTxRaw st),+    plain [] (\r -> PostTx <$> parseTxBody st r) (handlePostTx st),+    plainList limits (\r -> GetAddrTxs <$> cap r <*> lim r) (handleAddrTxs st),+    marshalled [] (\r -> GetAddrBalance <$> cap r) (handleAddrBalance st),+    marshalledList limits (\r -> GetAddrUnspent <$> cap r <*> lim r) (handleAddrUnspent st),+    plainList [] (\_ -> return GetPeers) (handlePeers st),+    pathHealth st,+    plainList ["limit", "offset"] (\r -> GetMempool <$> opt r <*> off r) (handleMempool st),+    marshalledList ["blocks", "notx"] (\r -> GetBlocks <$> need r <*> dflt r) (handleBlocks st),+    plain [] (\r -> GetBlockRaw <$> cap r) (handleBlockRaw st),+    plain [] (\_ -> return GetBlockBestRaw) (handleBlockBestRaw st),+    marshalledList ["notx"] (\r -> GetBlockLatest <$> dflt r) (handleBlockLatest st),+    marshalledList ["heights", "notx"] (\r -> GetBlockHeights <$> need r <*> dflt r) (handleBlockHeights st),+    plain [] (\r -> GetBlockHeightRaw <$> cap r) (handleBlockHeightRaw st),+    plain [] (\r -> GetBlockTimeRaw <$> cap r) (handleBlockTimeRaw st),+    plain [] (\r -> GetBlockMTPRaw <$> cap r) (handleBlockMTPRaw st),+    marshalledList ["txids"] (\r -> GetTxs <$> need r) (handleTxs st),+    plain ["txids"] (\r -> GetTxsRaw <$> need r) (handleTxsRaw st),+    marshalledList [] (\r -> GetTxsBlock <$> cap r) (handleTxsBlock st),+    plain [] (\r -> GetTxsBlockRaw <$> cap r) (handleTxsBlockRaw st),+    plain [] (\r -> GetTxAfter <$> cap r <*> cap r) (handleTxAfter st),+    plainList ("addresses" : limits) (\r -> GetAddrsTxs <$> need r <*> lim r) (handleAddrsTxs st),+    marshalledList limits (\r -> GetAddrTxsFull <$> cap r <*> lim r) (handleAddrTxsFull st),+    marshalledList ("addresses" : limits) (\r -> GetAddrsTxsFull <$> need r <*> lim r) (handleAddrsTxsFull st),+    marshalledList ["addresses"] (\r -> GetAddrsBalance <$> need r) (handleAddrsBalance st),+    marshalledList ("addresses" : limits) (\r -> GetAddrsUnspent <$> need r <*> lim r) (handleAddrsUnspent st),+    get "/events" ("pretty" : "after" : eventParams) (handleEvents st),+    get "/dbstats" [] (handleDbStats st),+    -- the reference of the API, as a page and as the file that it shows+    get "/" [] (\_ -> file "text/html; charset=utf-8" swaggerPage),+    get "/swagger.yaml" [] (\_ -> file "application/yaml" swaggerYaml)+  ]+    <> (if cfg.noXPub then [] else xpubRoutes)+    <> (if cfg.noBlockchainInfo then [] else binfoRoutes)+  where+    file kind bytes = return (responseLBS status200 [(hContentType, kind)] (L.fromStrict bytes))+    net = cfg.store.net+    -- the parameters that routes accept, beyond those of each route+    limits = ["limit", "offset", "height"]+    active = ["active", "activeP2SH", "activeBech32"]+    -- the Blockchain.info endpoints accept format=json ('jsonOnly')+    multiaddrParams =+      ["format", "onlyShow", "n", "offset", "noCompact", "no_compact", "currency", "cashaddr", "txidindex", "filter"]+        <> active+    rawaddrParams = ["format", "derive", "limit", "offset", "txidindex"]+    balanceParams = "format" : "cashaddr" : active+    unspentParams = "format" : "limit" : "confirmations" : "txidindex" : active+    historyParams = "format" : "start" : "end" : "currency" : active+    -- the parameters of a request+    cap, need :: (Param a) => Req -> IO a+    cap = paramCapture st+    need = paramRequired st+    dflt :: (Default a, Param a) => Req -> IO a+    dflt = paramDef st+    opt :: (Param a) => Req -> IO (Maybe a)+    opt = paramOptional st+    lim = parseLimits st+    off = parseOffset st+    -- routes by how they encode their responses: in the JSON of the+    -- network, as a list of those, or in plain JSON+    marshalled ::+      (ApiResource a b, MarshalJSON Network b) => [Text] -> (Req -> IO a) -> (a -> IO b) -> Route+    marshalled ps p h = pathCompact st ps p h (marshalEncoding net) (marshalValue net)+    marshalledList ::+      (ApiResource a (SerialList b), MarshalJSON Network b) => [Text] -> (Req -> IO a) -> (a -> IO [b]) -> Route+    marshalledList ps p h =+      pathCompact+        st+        ps+        p+        (fmap SerialList . h)+        (list (marshalEncoding net) . (.get))+        (toJSONList . map (marshalValue net) . (.get))+    plain :: (ApiResource a b, ToJSON b) => [Text] -> (Req -> IO a) -> (a -> IO b) -> Route+    plain ps p h = pathCompact st ps p h toEncoding toJSON+    plainList ::+      (ApiResource a (SerialList b), ToJSON b) => [Text] -> (Req -> IO a) -> (a -> IO [b]) -> Route+    plainList ps p h = plain ps p (fmap SerialList . h)+    xpubRoutes =+      [ plain ["derive", "nocache"] (\r -> GetXPub <$> cap r <*> dflt r <*> dflt r) (handleXPub st),+        plainList+          ("derive" : "nocache" : limits)+          (\r -> GetXPubTxs <$> cap r <*> dflt r <*> lim r <*> dflt r)+          (handleXPubTxs st),+        marshalledList+          ("derive" : "nocache" : limits)+          (\r -> GetXPubTxsFull <$> cap r <*> dflt r <*> lim r <*> dflt r)+          (handleXPubTxsFull st),+        marshalledList+          ["derive", "nocache"]+          (\r -> GetXPubBalances <$> cap r <*> dflt r <*> dflt r)+          (handleXPubBalances st),+        marshalledList+          ("derive" : "nocache" : limits)+          (\r -> GetXPubUnspent <$> cap r <*> dflt r <*> lim r <*> dflt r)+          (handleXPubUnspent st),+        plain ["derive"] (\r -> DelCachedXPub <$> cap r <*> dflt r) (handleDelXPub st)+      ]+    binfoRoutes =+      [ post "/blockchain/multiaddr" multiaddrParams (jsonOnly (handleMultiAddr st)),+        get "/blockchain/multiaddr" multiaddrParams (jsonOnly (handleMultiAddr st)),+        get "/blockchain/balance" balanceParams (jsonOnly (handleShortBal st)),+        post "/blockchain/balance" balanceParams (jsonOnly (handleShortBal st)),+        get "/blockchain/rawaddr/:addr" rawaddrParams (jsonOnly (handleRawAddr st)),+        get "/blockchain/address/:addr" rawaddrParams (jsonOnly (handleRawAddr st)),+        get "/blockchain/xpub/:addr" rawaddrParams (jsonOnly (handleRawAddr st)),+        post "/blockchain/unspent" unspentParams (jsonOnly (handleBinfoUnspent st)),+        get "/blockchain/unspent" unspentParams (jsonOnly (handleBinfoUnspent st)),+        get "/blockchain/rawtx/:txid" ["format", "txidindex"] (handleBinfoTx st),+        get "/blockchain/rawblock/:block" ["format", "txidindex"] (handleBinfoBlock st),+        get "/blockchain/latestblock" ["format", "txidindex"] (jsonOnly (handleBinfoLatest st)),+        get+          "/blockchain/unconfirmed-transactions"+          ["format", "limit", "offset", "txidindex"]+          (jsonOnly (handleBinfoMempool st)),+        get "/blockchain/block-height/:height" ["format", "txidindex"] (jsonOnly (handleBinfoBlockHeight st)),+        get "/blockchain/blocks/:milliseconds" ["format"] (jsonOnly (handleBinfoBlocksDay st)),+        get "/blockchain/export-history" historyParams (jsonOnly (handleBinfoHistory st)),+        post "/blockchain/export-history" historyParams (jsonOnly (handleBinfoHistory st)),+        get "/blockchain/q/addresstohash/:addr" [] (handleBinfoAddrToHash st),+        get "/blockchain/q/hashtoaddress/:hash" [] (handleBinfoHashToAddr st),+        get "/blockchain/q/addrpubkey/:pubkey" [] (handleBinfoAddrPubkey st),+        get "/blockchain/q/pubkeyaddr/:addr" [] (handleBinfoPubKeyAddr st),+        get "/blockchain/q/hashpubkey/:pubkey" [] (handleBinfoHashPubkey st),+        get "/blockchain/q/getblockcount" [] (handleBinfoGetBlockCount st),+        get "/blockchain/q/latesthash" [] (handleBinfoLatestHash st),+        get "/blockchain/q/bcperblock" [] (handleBinfoSubsidy st),+        get "/blockchain/q/txtotalbtcoutput/:txid" [] (handleBinfoTotalOut st),+        get "/blockchain/q/txtotalbtcinput/:txid" [] (handleBinfoTotalInput st),+        get "/blockchain/q/txfee/:txid" [] (handleBinfoTxFees st),+        get "/blockchain/q/txresult/:txid/:addr" [] (handleBinfoTxResult st),+        get "/blockchain/q/getreceivedbyaddress/:addr" [] (handleBinfoReceived st),+        get "/blockchain/q/getsentbyaddress/:addr" [] (handleBinfoSent st),+        get "/blockchain/q/addressbalance/:addr" [] (handleBinfoAddrBalance st),+        get "/blockchain/q/addressfirstseen/:addr" [] (handleFirstSeen st)+      ]++-- | A handler of the Blockchain.info endpoints that answer in JSON only. They+-- accept @format=json@, which clients written for Blockchain.info often+-- send, and refuse any other format rather than answering in JSON anyway.+jsonOnly :: (Req -> IO Response) -> Req -> IO Response+jsonOnly h req = do+  paramOr req json "format" ()+  h req+  where+    json "json" = Just ()+    json _ = Nothing++-- | Register a handler for an API resource, with compact JSON by default.+pathCompact ::+  (ApiResource a b) =>+  WebState ->+  [Text] ->+  (Req -> IO a) ->+  (a -> IO b) ->+  (b -> Encoding) ->+  (b -> Value) ->+  Route+pathCompact st ps parser action encJson encValue =+  pathCommon st ps parser action (const status200) encJson encValue False++-- | The route of an API resource: parse the request, run the handler, and+-- encode the response as JSON or binary, as the client asked. It accepts the+-- parameters given, and @pretty@.+pathCommon ::+  (ApiResource a b) =>+  WebState ->+  [Text] ->+  (Req -> IO a) ->+  (a -> IO b) ->+  (b -> Status) ->+  (b -> Encoding) ->+  (b -> Value) ->+  Bool ->+  Route+pathCommon _ ps parser action status encJson encValue pretty =+  route (renderStdMethod (resourceMethod proxy)) (capturePath proxy) ("pretty" : ps) $ \req -> do+    proto <- serialAs req pretty+    res <- action =<< parser req+    return $+      responseLBS (status res) [(hContentType, serialType proto)] (protoSerial proto encJson encValue res)+  where+    toProxy :: (Req -> IO a) -> Proxy a+    toProxy = const Proxy+    proxy = toProxy parser++-- | Respond with JSON.+streamEncoding :: Encoding -> IO Response+streamEncoding e =+  return $+    responseLBS status200 [(hContentType, "application/json; charset=utf-8")] (encodingToLazyByteString e)++-- | Respond with text.+textResponse :: TL.Text -> IO Response+textResponse t =+  return $+    responseLBS status200 [(hContentType, "text/plain; charset=utf-8")] (TL.encodeUtf8 t)++-- | Encode a response in binary, JSON or pretty JSON.+protoSerial ::+  (Serial a) =>+  SerialAs ->+  (a -> Encoding) ->+  (a -> Value) ->+  a ->+  L.ByteString+protoSerial SerialAsBinary _ _ = runPutL . serialize+protoSerial SerialAsJSON f _ = encodingToLazyByteString . f+protoSerial SerialAsPrettyJSON _ g =+  encodePretty' defConfig {confTrailingNewline = True} . g++-- | The content type of an encoding.+serialType :: SerialAs -> C.ByteString+serialType SerialAsBinary = "application/octet-stream"+serialType _ = "application/json"++-- | Whether a request asks for the binary encoding: its @Accept@ header+-- names @application\/octet-stream@, and gives it no less weight than it+-- gives JSON.+--+-- The header is a list of media types, each with a weight from 0 to 1,+-- written @;q=@, that is 1 when left out. Binary is what is sent when+-- @application\/octet-stream@ is in the list with a weight above 0 that is+-- at least that of @application\/json@, if that is there too. A wildcard+-- such as @*\/*@ asks for nothing in particular and gets JSON, the+-- default, as does a request without the header. The header used to be+-- compared with the one string, so that a client that sent a list, or a+-- weight, got JSON without being told.+wantsBinary :: Req -> Bool+wantsBinary req = maybe False acceptsBinary (Router.header req "accept")++-- | 'wantsBinary', given the value of the @Accept@ header.+acceptsBinary :: Text -> Bool+acceptsBinary accept =+  case weight "application/octet-stream" of+    Just q | q > 0 -> maybe True (q >=) (weight "application/json")+    _ -> False+  where+    ranges = map range (T.splitOn "," accept)+    weight name = lookup name ranges+    range :: Text -> (Text, Double)+    range part = case map T.strip (T.splitOn ";" part) of+      [] -> ("", 0)+      name : params -> (T.toLower name, fromMaybe 1 (listToMaybe (mapMaybe quality params)))+    quality setting = case T.splitOn "=" setting of+      [k, v] | T.toLower (T.strip k) == "q" -> readMaybe (T.unpack (T.strip v)) <|> Just 0+      _ -> Nothing++-- | The encoding that a request asks for: binary if it 'wantsBinary', else+-- JSON, pretty if the @pretty@ parameter or the default says so.+serialAs :: Req -> Bool -> IO SerialAs+serialAs req pretty+  | wantsBinary req = return SerialAsBinary+  | otherwise = do+      p <- paramOr req readBool "pretty" pretty+      return $ if p then SerialAsPrettyJSON else SerialAsJSON++-- GET Block / GET Blocks --++-- | Handler for @GET \/block\/:block@: a block by hash.+handleBlock ::+  WebState ->+  GetBlock ->+  IO BlockData+handleBlock st (GetBlock h (NoTx noTx)) = withMetrics st (.block) $ do+  st.query.getBlock h >>= \case+    Nothing ->+      raise st ThingNotFound+    Just b -> do+      return $ pruneTx noTx b++-- | Blocks by hash, without duplicates, leaving out unknown ones.+getBlocks ::+  WebState ->+  [H.BlockHash] ->+  Bool ->+  IO [BlockData]+getBlocks st hs notx =+  (pruneTx notx <$>) . catMaybes <$> mapM (st.query.getBlock) (nub hs)++-- | Handler for @GET \/blocks?blocks=…@: several blocks by hash.+handleBlocks ::+  WebState ->+  GetBlocks ->+  IO [BlockData]+handleBlocks st (GetBlocks hs (NoTx notx)) =+  withMetrics st (.block) $+    getBlocks st hs notx++-- | Keep only the coinbase in the list of transactions of a block, if the flag is 'True'.+pruneTx :: Bool -> BlockData -> BlockData+pruneTx False b = b+pruneTx True BlockData {..} = BlockData {txs = take 1 txs, ..}++-- GET BlockRaw --++-- | Handler for @GET \/block\/:block\/raw@: a block by hash, serialized as in the peer-to-peer protocol.+handleBlockRaw ::+  WebState ->+  GetBlockRaw ->+  IO (RawResult H.Block)+handleBlockRaw st (GetBlockRaw h) =+  withMetrics st (.blockRaw) $+    RawResult <$> getRawBlock st h++-- | Block by hash with all its transactions. Responds with an error if not found.+getRawBlock ::+  WebState ->+  H.BlockHash ->+  IO H.Block+getRawBlock st h =+  st.query.getBlock h >>= maybe (raise st ThingNotFound) return >>= toRawBlock st.query++-- | Rebuild a block from its data and its transactions.+toRawBlock :: StoreRead -> BlockData -> IO H.Block+toRawBlock q b = do+  txs <- map transactionData . catMaybes <$> C.getTransactions q b.txs+  return H.Block {header = b.header, txs}++-- GET BlockBest / BlockBestRaw --++-- | Handler for @GET \/block\/best@: the best block.+handleBlockBest ::+  WebState ->+  GetBlockBest ->+  IO BlockData+handleBlockBest st (GetBlockBest (NoTx notx)) =+  withMetrics st (.block) $+    (bestBlock st.query) >>= maybe (raise st ThingNotFound) (return . pruneTx notx)++-- | Handler for @GET \/block\/best\/raw@: the best block, serialized.+handleBlockBestRaw ::+  WebState ->+  GetBlockBestRaw ->+  IO (RawResult H.Block)+handleBlockBestRaw st _ =+  withMetrics st (.blockRaw) $+    st.query.getBestBlock >>= \case+      Nothing -> raise st ThingNotFound+      Just h -> RawResult <$> getRawBlock st h++-- GET BlockLatest --++-- | Handler for @GET \/block\/latest@: the best block and its ancestors, up to 100 blocks.+handleBlockLatest ::+  WebState ->+  GetBlockLatest ->+  IO [BlockData]+handleBlockLatest st (GetBlockLatest (NoTx noTx)) =+  withMetrics st (.block) $+    st.query.getBestBlock >>= maybe (raise st ThingNotFound) (go [] <=< st.query.getBlock)+  where+    go acc Nothing = return $ reverse acc+    go acc (Just b)+      | b.height <= 0 = return $ reverse acc+      | length acc == 99 = return . reverse $ pruneTx noTx b : acc+      | otherwise = go (pruneTx noTx b : acc) =<< st.query.getBlock b.header.prev++-- GET BlockHeight / BlockHeights / BlockHeightRaw --++-- | Handler for @GET \/block\/height\/:height@: the blocks at a height.+handleBlockHeight ::+  WebState ->+  GetBlockHeight ->+  IO [BlockData]+handleBlockHeight st (GetBlockHeight h (NoTx notx)) =+  withMetrics st (.block) $+    st.query.getBlocksAtHeight (fromIntegral h)+      >>= (\hs -> getBlocks st hs notx)++-- | Handler for @GET \/block\/heights?heights=…@: the blocks at several heights.+handleBlockHeights ::+  WebState ->+  GetBlockHeights ->+  IO [BlockData]+handleBlockHeights st (GetBlockHeights (HeightsParam heights) (NoTx notx)) =+  withMetrics st (.block) $+    mapM (st.query.getBlocksAtHeight . fromIntegral) heights+      >>= (\hs -> getBlocks st hs notx) . concat++-- | Handler for @GET \/block\/height\/:height\/raw@: the blocks at a height, serialized.+handleBlockHeightRaw ::+  WebState ->+  GetBlockHeightRaw ->+  IO (RawResultList H.Block)+handleBlockHeightRaw st (GetBlockHeightRaw h) =+  withMetrics st (.blockRaw) $+    fmap RawResultList $+      mapM (getRawBlock st) =<< st.query.getBlocksAtHeight (fromIntegral h)++-- GET BlockTime / BlockTimeRaw --++-- | Handler for @GET \/block\/time\/:time@: the last block with a timestamp at or before a time.+handleBlockTime ::+  WebState ->+  GetBlockTime ->+  IO BlockData+handleBlockTime st (GetBlockTime (TimeParam t) (NoTx notx)) =+  withMetrics st (.block) $ do+    let ch = st.config.store.chain+    C.blockAtOrBefore st.query ch t >>= \case+      Nothing -> raise st ThingNotFound+      Just b -> return $ pruneTx notx b++-- | Handler for @GET \/block\/mtp\/:time@: the first block with a median time past at or after a time.+handleBlockMTP ::+  WebState ->+  GetBlockMTP ->+  IO BlockData+handleBlockMTP st (GetBlockMTP (TimeParam t) (NoTx notx)) =+  withMetrics st (.block) $ do+    let ch = st.config.store.chain+    C.blockAtOrAfterMTP st.query ch t >>= \case+      Nothing -> raise st ThingNotFound+      Just b -> return $ pruneTx notx b++-- | Handler for @GET \/block\/time\/:time\/raw@: the last block with a timestamp at or before a time, serialized.+handleBlockTimeRaw ::+  WebState ->+  GetBlockTimeRaw ->+  IO (RawResult H.Block)+handleBlockTimeRaw st (GetBlockTimeRaw (TimeParam t)) =+  withMetrics st (.blockRaw) $ do+    let ch = st.config.store.chain+    C.blockAtOrBefore st.query ch t >>= \case+      Nothing -> raise st ThingNotFound+      Just b -> RawResult <$> (toRawBlock st.query b)++-- | Handler for @GET \/block\/mtp\/:time\/raw@: the first block with a median time+-- past at or after a time, serialized.+handleBlockMTPRaw ::+  WebState ->+  GetBlockMTPRaw ->+  IO (RawResult H.Block)+handleBlockMTPRaw st (GetBlockMTPRaw (TimeParam t)) =+  withMetrics st (.blockRaw) $ do+    let ch = st.config.store.chain+    C.blockAtOrAfterMTP st.query ch t >>= \case+      Nothing -> raise st ThingNotFound+      Just b -> RawResult <$> (toRawBlock st.query b)++-- GET Transactions --++-- | Handler for @GET \/transaction\/:txid@: a transaction.+handleTx :: WebState -> GetTx -> IO Transaction+handleTx st (GetTx txid) =+  withMetrics st (.tx) $+    C.getTransaction st.query txid >>= maybe (raise st ThingNotFound) return++-- | Handler for @GET \/transactions?txids=…@: several transactions.+handleTxs ::+  WebState ->+  GetTxs ->+  IO [Transaction]+handleTxs st (GetTxs txids) =+  withMetrics st (.tx) $+    catMaybes <$> C.getTransactions st.query (nub txids)++-- | Handler for @GET \/transaction\/:txid\/raw@: a transaction, serialized.+handleTxRaw ::+  WebState ->+  GetTxRaw ->+  IO (RawResult Tx)+handleTxRaw st (GetTxRaw txid) =+  withMetrics st (.txRaw) $+    C.getTransaction st.query txid+      >>= \case+        Nothing -> raise st ThingNotFound+        Just tx -> return $ RawResult (transactionData tx)++-- | Handler for @GET \/transactions\/raw?txids=…@: several transactions, serialized.+handleTxsRaw ::+  WebState ->+  GetTxsRaw ->+  IO (RawResultList Tx)+handleTxsRaw st (GetTxsRaw txids) =+  withMetrics st (.txRaw) $+    RawResultList . map transactionData . catMaybes+      <$> C.getTransactions st.query (nub txids)++-- | Transactions of a block. Responds with an error if not found.+getTxsBlock ::+  WebState ->+  H.BlockHash ->+  IO [Transaction]+getTxsBlock st h =+  st.query.getBlock h >>= \case+    Nothing -> raise st ThingNotFound+    Just b -> catMaybes <$> C.getTransactions st.query b.txs++-- | Handler for @GET \/transactions\/block\/:block@: the transactions of a block.+handleTxsBlock ::+  WebState ->+  GetTxsBlock ->+  IO [Transaction]+handleTxsBlock st (GetTxsBlock h) =+  withMetrics st (.txBlock) $ getTxsBlock st h++-- | Handler for @GET \/transactions\/block\/:block\/raw@: the transactions of a block, serialized.+handleTxsBlockRaw ::+  WebState ->+  GetTxsBlockRaw ->+  IO (RawResultList Tx)+handleTxsBlockRaw st (GetTxsBlockRaw h) =+  withMetrics st (.txBlockRaw) $+    RawResultList . map transactionData <$> getTxsBlock st h++-- GET TransactionAfterHeight --++-- | Handler for @GET \/transaction\/:txid\/after\/:height@: whether any ancestor+-- of a transaction is a coinbase confirmed after a height.+handleTxAfter ::+  WebState ->+  GetTxAfter ->+  IO (GenericResult (Maybe Bool))+handleTxAfter st (GetTxAfter txid height) =+  withMetrics st (.txAfter) $+    GenericResult . fst <$> (cbAfterHeight st.query (fromIntegral height) txid)++-- | Check if any of the ancestors of this transaction is a coinbase after the+-- specified height. Returns 'Nothing' if answer cannot be computed before+-- hitting limits.+cbAfterHeight ::+  StoreRead ->+  H.BlockHeight ->+  TxHash ->+  IO (Maybe Bool, Int)+cbAfterHeight q height txid =+  inputs n HashSet.empty HashSet.empty [txid]+  where+    n = 10000+    inputs 0 _ _ [] = return (Nothing, 10000)+    inputs i is ns [] =+      let is' = HashSet.union is ns+          ns' = HashSet.empty+          ts = HashSet.toList (HashSet.difference ns is)+       in case ts of+            [] -> return (Just False, n - i)+            _ -> inputs i is' ns' ts+    inputs i is ns (t : ts) =+      C.getTransaction q t >>= \case+        Nothing -> return (Nothing, n - i)+        Just tx+          | height_check tx ->+              if cb_check tx+                then return (Just True, n - i + 1)+                else+                  let ns' = HashSet.union (ins tx) ns+                   in inputs (i - 1) is ns' ts+          | otherwise -> inputs (i - 1) is ns ts+    cb_check = any isCoinbase . (.inputs)+    ins = HashSet.fromList . map (.outpoint.hash) . (.inputs)+    height_check tx =+      case tx.block of+        BlockRef h _ -> h > height+        MemRef _ -> True++-- POST Transaction --++-- | Handler for @POST \/transactions@: broadcast a transaction to half of the peers and return its hash.+handlePostTx :: WebState -> PostTx -> IO TxId+handlePostTx st (PostTx tx) =+  withMetrics st (.txPost) $ do+    let cfg = st.config+    (publishTx cfg tx)+    return (TxId (txHash tx))++-- | Send transaction to all connected peers.+publishTx ::+  WebConfig ->+  Tx ->+  IO ()+publishTx cfg tx = do+  ps <- getPeers cfg.store.peerMgr+  let c = max 1 (length ps `div` 2)+  forM_ (take c ps) $ \p -> do+    sendMessage (MTx tx) p.mailbox+    void . async $ do+      threadDelay (5 * 1000 * 1000)+      let v = if cfg.store.net.segWit then InvWitnessTx else InvTx+          g = MGetData (GetData [InvVector v (txHash tx).get])+      sendMessage g p.mailbox++-- GET Mempool / Events --++-- | Handler for @GET \/mempool@: hashes of mempool transactions, newest first.+handleMempool ::+  WebState ->+  GetMempool ->+  IO [TxHash]+handleMempool st (GetMempool limitM (OffsetParam o)) =+  withMetrics st (.mempool) $ do+    WebLimits {..} <- pure st.config.limits+    let wl' = WebLimits {maxItemCount = 0, ..}+        l = Limits (validateLimit wl' False limitM) (fromIntegral o) Nothing+    map snd <$> st.query.getMempoolPage l++-- | Middleware that serves @\/events@ over a WebSocket: each new block and+-- each transaction that enters or leaves the mempool is a message, from the+-- log of events ("Haskoin.Store.Web.Events").+--+-- The client may send the commands of t'EventCommand' to choose the+-- transactions that it hears about. Each side speaks JSON in text frames+-- or binary in binary frames, and the server speaks as it was last spoken+-- to; before the client has said anything, as the @Accept@ header of the+-- handshake asks, like any request. A command that the server cannot read+-- or cannot do closes the connection with the reason, as a node does with+-- an SPV client: there are no answers to wait for.+--+-- @?after=@ with the hash of the last event heard takes up from there+-- ('eventsStart'). Clients that do not answer pings, or that fall off the+-- start of the log, are disconnected.+webSocketEvents :: WebState -> Middleware+webSocketEvents s =+  websocketsOr defaultConnectionOptions (wrap . events)+  where+    pingPong = defaultPingPongOptions {pingInterval = 30, pongTimeout = 60}+    gauge = (.events) <$> s.metrics+    net = s.config.store.net+    ctx = s.config.store.ctx+    gap = s.query.maxGap+    wrap f = case gauge of+      Nothing -> f+      Just g -> withGaugeIO g f+    events pending = do+      let request = pendingRequest pending+          (path, query) = C.break (== '?') (requestPath request)+          asked = parseQuery query+          accept = lookup "Accept" (WebSockets.requestHeaders request)+          binary = maybe False (acceptsBinary . T.decodeUtf8Lenient) accept+      if path /= "/events"+        then turnAway pending ThingNotFound+        else+          eventsOpen s (\name -> T.decodeUtf8Lenient <$> join (lookup (T.encodeUtf8 name) asked)) >>= \case+            Left e -> turnAway pending e+            Right (start, watch) -> do+              conn <- acceptRequest pending+              format <- newTVarIO binary+              -- websockets only sees pongs while reading, which the+              -- thread that hears the commands does+              withPingPong pingPong conn $ \c ->+                race_ (hear c watch format) (tell c watch format start) `catch` gone+    -- a client that leaves is not an error to report+    gone :: WebSockets.ConnectionException -> IO ()+    gone _ = return ()+    -- the commands of the client, until one cannot be done+    hear c watch format = do+      message <- WebSockets.receiveDataMessage c+      let (binary, read') = case message of+            WebSockets.Text bytes _ -> (False, A.decode bytes >>= parseMaybe (unmarshalValue (net, ctx)))+            WebSockets.Binary bytes -> (True, either (const Nothing) Just (unmarshal (net, ctx) (L.toStrict bytes)))+      atomically (writeTVar format binary)+      case read' of+        Nothing -> refuse c "Could not read the command"+        Just cmd ->+          try (command ctx gap s.query.xPubBals watch cmd) >>= \case+            Right (Right ()) -> hear c watch format+            Right (Left why) -> refuse c why+            Left (_ :: Except) -> refuse c "Could not do the command"+    -- 1008: the message is against what the server allows+    refuse c why = WebSockets.sendCloseCode c 1008 (T.pack why)+    -- the events from a place in the log on, those that the client chose+    tell c watch format from =+      atomically (readEvents s.eventLog from) >>= \case+        Nothing -> WebSockets.sendClose c ("Event queue full" :: Text)+        Just entries -> do+          forM_ entries $ \e -> do+            wanted <- atomically (stateTVar watch (relevant ctx gap e.seen))+            when wanted $+              readTVarIO format >>= \case+                True -> sendBinaryData c (runPutL (serialize e.event))+                False -> sendTextData c (A.encode e.event)+          tell c watch format (from + fromIntegral (length entries))+    turnAway pending e =+      rejectRequestWith+        pending+        WebSockets.defaultRejectRequest+          { WebSockets.rejectBody = L.toStrict (A.encode e),+            WebSockets.rejectCode = statusCode (errStatus e),+            WebSockets.rejectMessage = statusMessage (errStatus e),+            WebSockets.rejectHeaders = [("Content-Type", "application/json")]+          }++-- | Handler for @GET \/events@: stream of new blocks and of transactions+-- that enter or leave the mempool, one per line in JSON or one after the+-- other in binary, until the client falls off the start of the log.+-- @after@ takes up from an event ('eventsStart').+handleEvents :: WebState -> Req -> IO Response+handleEvents st req = do+  proto <- serialAs req False+  let asked name = lookup name req.params+  (start, watch) <- eventsOpen st asked >>= either (raise st) return+  return . responseStream status200 [(hContentType, serialType proto)] $ \write flush' ->+    let send from = do+          flush'+          -- ends the response when the client falls too far behind+          atomically (readEvents st.eventLog from) >>= \case+            Nothing -> return ()+            Just entries -> do+              forM_ entries $ \e -> do+                wanted <- atomically (stateTVar watch (relevant own.ctx st.query.maxGap e.seen))+                when wanted (write (serial proto e.event))+              send (from + fromIntegral (length entries))+     in withGaugeIncrease st (.events) (send start)+  where+    serial proto e =+      lazyByteString $+        protoSerial proto toEncoding toJSON e <> newLine proto+    newLine SerialAsBinary = mempty+    newLine SerialAsJSON = "\n"+    newLine SerialAsPrettyJSON = mempty+    own = st.config.store++-- | Where a client of the events starts reading the log, and what it hears+-- about from the start, as the parameters of its request say: 'eventsStart'+-- for @after@, and for the rest a choice made of the same commands that a+-- WebSocket client sends later.+--+-- A WebSocket client could only choose once it was connected, and until its+-- first command was read it was sent every transaction of a busy mempool.+-- With these the choice is there before the first event:+--+-- * @quiet=true@: no transactions, only blocks, until something is watched;+-- * @addresses@: a list of addresses to watch, separated by commas;+-- * @xpub@ and @derive@: an extended public key and the types of its+--   addresses, one or several separated by commas, legacy if left out;+-- * @filter@, @functions@, @tweak@, @flags@: a Bloom filter, in hex, with+--   its numbers.+--+-- Any of them but @quiet=false@ makes the client a filtered one. The HTTP+-- stream takes them too, and it is the only way that it has to choose.+-- The place in the log is taken first, so that nothing that happens while+-- a key is read from the database is left out.+eventsOpen :: WebState -> (Text -> Maybe Text) -> IO (Either Except (Word64, TVar Watch))+eventsOpen st asked =+  eventsStart st (asked "after") >>= \case+    Left e -> return (Left e)+    Right start -> case commands of+      Left why -> return (Left (UserError why))+      Right cmds -> do+        watch <- newTVarIO everything+        let run [] = return (Right (start, watch))+            run (cmd : rest) =+              command own.ctx st.query.maxGap st.query.xPubBals watch cmd >>= \case+                Left why -> return (Left (UserError why))+                Right () -> run rest+        run cmds+  where+    own = st.config.store+    commands = do+      quiet <- maybe (Right False) (need "quiet" . readBool) (asked "quiet")+      addresses <- mapM (need "addresses" . textToAddr own.net) (listed "addresses")+      derivs <- mapM (need "derive" . textToDeriveType) (listed "derive")+      keys <- mapM (need "xpub" . xPubImport own.net own.ctx) (maybe [] pure (asked "xpub"))+      filters <- case asked "filter" of+        Nothing -> Right []+        Just hex -> do+          let number name = maybe (Right 0) (need name . (readNum :: Text -> Maybe Word32)) (asked name)+          functions <- number "functions"+          tweak <- number "tweak"+          flags <- number "flags"+          let json =+                A.object+                  [ "type" A..= A.String "filterload",+                    "filter" A..= hex,+                    "functions" A..= functions,+                    "tweak" A..= tweak,+                    "flags" A..= flags+                  ]+          pure <$> need "filter" (parseMaybe (unmarshalValue (own.net, own.ctx)) json)+      when (null keys && not (null derivs)) (Left "Parameter derive needs xpub")+      let types = if null derivs then [DeriveNormal] else derivs+      return $+        [EventNone | quiet]+          <> [EventWatch addresses | not (null addresses)]+          <> [EventWatchXPub XPubSpec {key, deriv} | key <- keys, deriv <- types]+          <> filters+    listed name = maybe [] (filter (not . T.null) . T.splitOn ",") (asked name)+    need :: Text -> Maybe a -> Either String a+    need name = maybe (Left ("Could not read parameter " <> T.unpack name)) Right++-- | Where a client of the events starts reading the log: at the end, or+-- after the event that it says it heard last, named by the hash of its+-- block or transaction. 404 when the log has no event with that hash: the+-- client cannot know what it missed, and has to read what it follows again+-- and listen from the end.+eventsStart :: WebState -> Maybe Text -> IO (Either Except Word64)+eventsStart st = \case+  Nothing -> Right <$> atomically (eventsEnd st.eventLog)+  Just hex -> case (H.hexToBlockHash hex, hexToTxHash hex) of+    (Nothing, Nothing) -> return (Left (UserError "The parameter after is not a hash"))+    (block, tx) -> maybe (Left ThingNotFound) Right <$> atomically (eventsAfter st.eventLog block tx)++-- | Write what happens into the log of events for as long as the server+-- runs, each transaction that enters with what the database has of it+-- ('seenOf'), for the clients that choose what they hear about.+--+-- A store alone writes what it publishes. A member of a fleet writes what+-- the fleet tells ('memberEvents'), from the start of what Redis keeps, so+-- that the logs of all the servers of a fleet hold the same events and a+-- client can take up on any of them.+feedEvents :: WebState -> IO ()+feedEvents st = case own.fleet of+  Just member -> memberEvents member True (mapM_ happened . fleet) complain+  Nothing -> do+    queue <- newTQueueIO+    let listen = mapM_ (writeTQueue queue) . local+    bracket (subscribe own.pub listen) (unsubscribe own.pub) $ \_ ->+      forever (atomically (readTQueue queue) >>= happened)+  where+    own = st.config.store+    local = \case+      StoreBestBlock b -> Just (Left b)+      StoreMempoolNew t -> Just (Right (t, True))+      StoreMempoolDelete t -> Just (Right (t, False))+      _ -> Nothing+    fleet = \case+      FleetBlock h -> Left <$> hash h+      FleetEntered h -> Right . (,True) <$> hash h+      FleetLeft h -> Right . (,False) <$> hash h+    hash :: (Serialize x) => C.ByteString -> Maybe x+    hash = either (const Nothing) Just . decode+    happened = \case+      Left b -> atomically (appendEvent st.eventLog (BlockArrived b))+      Right (t, False) -> atomically (appendEvent st.eventLog (TxLeft t))+      Right (t, True) -> do+        -- a transaction that cannot be read is told to every client+        known <- st.direct.getTxData t `catchAny` \_ -> return Nothing+        atomically (appendEvent st.eventLog (TxEntered t (seenOf own.net own.ctx <$> known)))+    complain why = logError (withSource "Events" st.config.logger) ("Could not read the events: " <> cs why)++-- GET Address Transactions --++-- | Handler for @GET \/address\/:address\/transactions@: transactions of an address.+handleAddrTxs ::+  WebState ->+  GetAddrTxs ->+  IO [TxRef]+handleAddrTxs st (GetAddrTxs addr pLimits) =+  withMetrics st (.addressTx) $+    st.query.getAddressTxs addr =<< paramToLimits st False pLimits++-- | Handler for @GET \/address\/transactions?addresses=…@: transactions of several addresses.+handleAddrsTxs ::+  WebState ->+  GetAddrsTxs ->+  IO [TxRef]+handleAddrsTxs st (GetAddrsTxs addrs pLimits) =+  withMetrics st (.addressTx) $+    st.query.getAddressesTxs addrs =<< paramToLimits st False pLimits++-- | Handler for @GET \/address\/:address\/transactions\/full@: full transactions of an address.+handleAddrTxsFull ::+  WebState ->+  GetAddrTxsFull ->+  IO [Transaction]+handleAddrTxsFull st (GetAddrTxsFull addr pLimits) =+  withMetrics st (.addressTxFull) $ do+    txs <- st.query.getAddressTxs addr =<< paramToLimits st True pLimits+    catMaybes <$> C.getTransactions st.query (map (.txid) txs)++-- | Handler for @GET \/address\/transactions\/full?addresses=…@: full transactions of several addresses.+handleAddrsTxsFull ::+  WebState ->+  GetAddrsTxsFull ->+  IO [Transaction]+handleAddrsTxsFull st (GetAddrsTxsFull addrs pLimits) =+  withMetrics st (.addressTxFull) $ do+    txs <- st.query.getAddressesTxs addrs =<< paramToLimits st True pLimits+    catMaybes <$> C.getTransactions st.query (map (.txid) txs)++-- | Handler for @GET \/address\/:address\/balance@: balance of an address.+handleAddrBalance :: WebState -> GetAddrBalance -> IO Balance+handleAddrBalance st (GetAddrBalance addr) =+  withMetrics st (.addressBalance) $ C.getDefaultBalance st.query addr++-- | Handler for @GET \/address\/balances?addresses=…@: balances of several addresses.+handleAddrsBalance ::+  WebState ->+  GetAddrsBalance ->+  IO [Balance]+handleAddrsBalance st (GetAddrsBalance addrs) =+  withMetrics st (.addressBalance) $ st.query.getBalances addrs++-- | Handler for @GET \/address\/:address\/unspent@: unspent outputs of an address.+handleAddrUnspent ::+  WebState ->+  GetAddrUnspent ->+  IO [Unspent]+handleAddrUnspent st (GetAddrUnspent addr pLimits) =+  withMetrics st (.addressUnspent) $+    st.query.getAddressUnspents addr =<< paramToLimits st False pLimits++-- | Handler for @GET \/address\/unspent?addresses=…@: unspent outputs of several addresses.+handleAddrsUnspent ::+  WebState ->+  GetAddrsUnspent ->+  IO [Unspent]+handleAddrsUnspent st (GetAddrsUnspent addrs pLimits) =+  withMetrics st (.addressUnspent) $+    st.query.getAddressesUnspents addrs =<< paramToLimits st False pLimits++-- GET XPubs --++-- | Handler for @GET \/xpub\/:xpub@: summary of an extended public key.+handleXPub ::+  WebState ->+  GetXPub ->+  IO XPubSummary+handleXPub st (GetXPub xpub deriv (NoCache noCache)) =+  withMetrics st (.xpub) $+    let xs = XPubSpec xpub deriv+     in xPubSummary xs <$> xPubBalsChecked st noCache xs++-- | Handler for @DELETE \/xpub\/:xpub@: remove an extended public key from the cache. Returns 'True' if it was cached.+handleDelXPub ::+  WebState ->+  DelCachedXPub ->+  IO (GenericResult Bool)+handleDelXPub st (DelCachedXPub xpub deriv) =+  withMetrics st (.xpubDelete) $ do+    let c = st.config.store.cache+    r <- maybe (return False) (`cacheDelete` XPubSpec xpub deriv) c+    return $ GenericResult r++-- | Balances of the addresses of an extended public key ('xPubBals'), from+-- the cache unless told not to, refusing a wrong kind of address+-- ('checkDerive').+xPubBalsChecked :: WebState -> Bool -> XPubSpec -> IO [XPubBal]+xPubBalsChecked st noCache xs = do+  bals <- (cacheOr noCache st).query.xPubBals xs+  checkDerive st xs bals+  return bals++-- | Answer with an error if an extended public key has no transactions with+-- the kind of address asked for, but has some with another kind: the request+-- named the wrong kind.+--+-- An xpub does not record which kind of address its wallet derives, and the+-- same key gives different addresses for each kind, so a request with the+-- wrong @derive@ would otherwise get an empty wallet: a valid answer, and a+-- wrong one, that a wallet would show its user as a zero balance. An error+-- says what went wrong and how to fix it.+--+-- The check costs nothing for a wallet that has transactions. For one that+-- looks unused, it reads the balances of the first addresses of each other+-- kind, as many as the store reads to decide that an xpub is unused, in one+-- read per kind. A wallet unused with every kind is a new wallet, and gets its+-- empty answer. Networks without SegWit have one kind only, and no check.+checkDerive :: WebState -> XPubSpec -> [XPubBal] -> IO ()+checkDerive st xs bals =+  when (net.segWit && all (\b -> b.balance.txs == 0) bals) $ do+    used <- filterM usedAs others+    forM_ (take 1 used) $ \d ->+      raise st . UserError $+        unwords+          [ "This xpub has no transactions with derive=" <> name xs.deriv <> ",",+            "but has some with derive=" <> name d <> ";",+            "ask with derive=" <> name d <> "."+          ]+  where+    net = st.config.store.net+    ctx = st.config.store.ctx+    others = filter (/= xs.deriv) [DeriveNormal, DeriveP2SH, DeriveP2WPKH]+    name = T.unpack . deriveTypeToText+    usedAs d = do+      let external = pubSubKey ctx xs.key 0+          addrs =+            map snd . take (fromIntegral st.query.initialGap) $+              deriveAddresses (deriveFunction ctx d) external 0+      any (\b -> b.txs > 0) <$> st.query.getBalances addrs++-- | Transactions of an extended public key, optionally without the cache.+getXPubTxs ::+  WebState ->+  XPubKey ->+  DeriveType ->+  LimitsParam ->+  Bool ->+  IO [TxRef]+getXPubTxs st xpub deriv plimits nocache = do+  limits <- paramToLimits st False plimits+  let xs = XPubSpec xpub deriv+      st' = cacheOr nocache st+  xbals <- xPubBalsChecked st nocache xs+  st'.query.xPubTxs xs xbals limits++-- | Handler for @GET \/xpub\/:xpub\/transactions@: transactions of an extended public key.+handleXPubTxs ::+  WebState ->+  GetXPubTxs ->+  IO [TxRef]+handleXPubTxs st (GetXPubTxs xpub deriv plimits (NoCache nocache)) =+  withMetrics st (.xpubTx) $+    getXPubTxs st xpub deriv plimits nocache++-- | Handler for @GET \/xpub\/:xpub\/transactions\/full@: full transactions of an extended public key.+handleXPubTxsFull ::+  WebState ->+  GetXPubTxsFull ->+  IO [Transaction]+handleXPubTxsFull st (GetXPubTxsFull xpub deriv plimits (NoCache nocache)) =+  withMetrics st (.xpubTxFull) $+    fmap catMaybes $+      C.getTransactions (cacheOr nocache st).query . map (.txid)+        =<< getXPubTxs st xpub deriv plimits nocache++-- | Handler for @GET \/xpub\/:xpub\/balances@: balances of the used addresses of an extended public key.+handleXPubBalances ::+  WebState ->+  GetXPubBalances ->+  IO [XPubBal]+handleXPubBalances st (GetXPubBalances xpub deriv (NoCache noCache)) =+  withMetrics st (.xpubBalance) $+    xPubBalsChecked st noCache (XPubSpec xpub deriv)++-- | Handler for @GET \/xpub\/:xpub\/unspent@: unspent outputs of an extended public key.+handleXPubUnspent ::+  WebState ->+  GetXPubUnspent ->+  IO [XPubUnspent]+handleXPubUnspent st (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) =+  withMetrics st (.xpubUnspent) $ do+    limits <- paramToLimits st False pLimits+    let xspec = XPubSpec xpub deriv+        st' = cacheOr noCache st+    xbals <- xPubBalsChecked st noCache xspec+    st'.query.xPubUnspents xspec xbals limits++---------------------------------------+-- Blockchain.info API Compatibility --+---------------------------------------++-- | Currency symbol of the coin of a network, in the Blockchain.info format.+netBinfoSymbol :: Network -> BinfoSymbol+netBinfoSymbol net+  | net == btc =+      BinfoSymbol+        { code = "BTC",+          symbol = "BTC",+          name = "Bitcoin",+          conversion = 100 * 1000 * 1000,+          after = True,+          local = False+        }+  | net == bch =+      BinfoSymbol+        { code = "BCH",+          symbol = "BCH",+          name = "Bitcoin Cash",+          conversion = 100 * 1000 * 1000,+          after = True,+          local = False+        }+  | otherwise =+      BinfoSymbol+        { code = "XTS",+          symbol = "¤",+          name = "Test",+          conversion = 100 * 1000 * 1000,+          after = False,+          local = False+        }++-- | Currency symbol of a fiat currency from its price ticker.+binfoTickerToSymbol :: Text -> BinfoTicker -> BinfoSymbol+binfoTickerToSymbol code t =+  BinfoSymbol+    { code,+      symbol = t.symbol,+      name,+      conversion = 100 * 1000 * 1000 / t.fifteen, -- sat/usd+      after = False,+      local = True+    }+  where+    name = case code of+      "EUR" -> "Euro"+      "USD" -> "U.S. dollar"+      "GBP" -> "British pound"+      x -> x++-- | Addresses and xpubs in a request parameter, separated by pipes or commas.+getBinfoAddrsParam ::+  WebState ->+  Req ->+  Text ->+  IO (HashSet BinfoAddr)+getBinfoAddrsParam st req name = do+  let net = st.config.store.net+  let ctx = st.config.store.ctx+  p <- fromMaybe "" <$> param req name+  if T.null p+    then return HashSet.empty+    else case parseBinfoAddr net ctx p of+      Nothing -> raise st (UserError "invalid address")+      Just xs -> return $ HashSet.fromList xs++-- | Xpubs and addresses in the @active@, @activeP2SH@ and @activeBech32@+-- parameters. Xpubs in each parameter derive legacy, nested SegWit and native+-- SegWit addresses.+getBinfoActive ::+  WebState ->+  Req ->+  IO (HashSet XPubSpec, HashSet Address)+getBinfoActive st req = do+  active <- getBinfoAddrsParam st req "active"+  p2sh <- getBinfoAddrsParam st req "activeP2SH"+  bech32 <- getBinfoAddrsParam st req "activeBech32"+  let xspec d b = (`XPubSpec` d) <$> xpub b+      xspecs =+        HashSet.fromList $+          concat+            [ mapMaybe (xspec DeriveNormal) (HashSet.toList active),+              mapMaybe (xspec DeriveP2SH) (HashSet.toList p2sh),+              mapMaybe (xspec DeriveP2WPKH) (HashSet.toList bech32)+            ]+      addrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList active+  return (xspecs, addrs)+  where+    addr (BinfoAddr a) = Just a+    addr (BinfoXpub _) = Nothing+    xpub (BinfoXpub x) = Just x+    xpub (BinfoAddr _) = Nothing++-- | A kind of address to derive, as in the @derive@ parameter of the xpub+-- endpoints: SegWit kinds only on networks with SegWit.+readDerive :: Network -> Text -> Maybe DeriveType+readDerive net t = do+  d <- textToDeriveType t+  guard (net.segWit || d == DeriveNormal)+  return d++-- | Whether to identify transactions by short number, unless @txidindex@ is false.+getNumTxId :: Req -> IO Bool+getNumTxId req = not <$> paramOr req readBool "txidindex" False++-- | Height of the best header.+getChainHeight :: WebState -> IO H.BlockHeight+getChainHeight st =+  fmap (.height) $ chainGetBest =<< pure st.config.store.chain++-- | Handler for @GET\/POST \/blockchain\/unspent@: unspent outputs of addresses and xpubs, as Blockchain.info.+handleBinfoUnspent :: WebState -> Req -> IO Response+handleBinfoUnspent st req =+  withMetrics st (.binfoUnspent) $ do+    (xspecs, addrs) <- getBinfoActive st req+    numtxid <- getNumTxId req+    limit <- get_limit+    min_conf <- get_min_conf+    let net = st.config.store.net+    let ctx = st.config.store.ctx+    height <- getChainHeight st+    let mn u = min_conf > u.confirmations+    xbals <- getXBals st.query xspecs+    bus <-+      runConduit $+        getBinfoUnspents st.query numtxid height xbals xspecs addrs+          .| (dropWhileC mn >> takeC limit .| sinkList)+    streamEncoding (marshalEncoding (net, ctx) (BinfoUnspents bus))+  where+    get_limit = min 1000 . fromIntegral <$> paramOr req readNat "limit" 250+    get_min_conf = fromIntegral <$> paramOr req readNat "confirmations" 0++-- | Stream the unspent outputs of xpubs and addresses, newest first, in the+-- Blockchain.info format.+getBinfoUnspents ::+  StoreRead ->+  Bool ->+  H.BlockHeight ->+  HashMap XPubSpec [XPubBal] ->+  HashSet XPubSpec ->+  HashSet Address ->+  ConduitT () BinfoUnspent IO ()+getBinfoUnspents q numtxid height xbals xspecs addrs = do+  cs' <- conduits+  joinDescStreams (unspentKey . fst) cs' .| mapC (uncurry binfo)+  where+    binfo u xp =+      let conf = case u.block of+            MemRef _ -> 0+            -- an output from the cache may be in a block that the chain of+            -- this store does not have yet: it is confirmed at least once+            BlockRef h _ -> if h > height then 1 else height - h + 1+       in BinfoUnspent+            { txid = u.outpoint.hash,+              index = u.outpoint.index,+              script = u.script,+              value = u.value,+              confirmations = fromIntegral conf,+              txidx = encodeBinfoTxId numtxid u.outpoint.hash,+              xpub = xp+            }+    conduits = (<>) <$> xconduits <*> pure acounduits+    xconduits = lift $ do+      let f x (XPubUnspent u p) =+            let path = toSoft (listToPath p)+                xp = BinfoXPubPath x.key <$> path+             in (u, xp)+          g x = do+            let h = q.xPubUnspents x (xBals x xbals)+                l = let Limits {..} = def in Limits {limit = 16, ..}+            return $ streamThings h Nothing l .| mapC (f x)+      mapM g (HashSet.toList xspecs)+    acounduits =+      let f u = (u, Nothing)+          h = q.getAddressUnspents+          l = let Limits {..} = def in Limits {limit = 16, ..}+          g a = streamThings (h a) Nothing l .| mapC f+       in map g (HashSet.toList addrs)++-- | Balances of the used addresses of each xpub.+getXBals ::+  StoreRead ->+  HashSet XPubSpec ->+  IO (HashMap XPubSpec [XPubBal])+getXBals q =+  fmap HashMap.fromList . mapM f . HashSet.toList+  where+    f x = (x,) . filter (not . nullBalance . (.balance)) <$> q.xPubBals x++-- | Balances of an xpub, or none if missing.+xBals :: XPubSpec -> HashMap XPubSpec [XPubBal] -> [XPubBal]+xBals = HashMap.findWithDefault []++-- | Stream the references to the transactions of xpubs and addresses, newest+-- first, without duplicates. Each xpub and address is read page by page, only+-- as far as the consumer needs, in pages of the given size: about as many as+-- the consumer is expected to take, as every page is a query of its own.+binfoTxRefs ::+  StoreRead ->+  Word32 ->+  HashMap XPubSpec [XPubBal] ->+  HashSet XPubSpec ->+  HashSet Address ->+  ConduitT () TxRef IO ()+binfoTxRefs q pageSize xbals sxspecs saddrs =+  joinDescStreams txRefKey $+    map xpub_c (HashSet.toList sxspecs) <> map addr_c (HashSet.toList saddrs)+  where+    l = Limits {limit = pageSize, offset = 0, start = Nothing}+    xpub_c x = streamThings (q.xPubTxs x (xBals x xbals)) (Just (.txid)) l+    addr_c a = streamThings (q.getAddressTxs a) (Just (.txid)) l++-- | Stream the transactions of xpubs and addresses, newest first, in the+-- Blockchain.info format, with the running balance of the given addresses after+-- each, keeping only those that pass the filter.+getBinfoTxs ::+  StoreRead ->+  Word32 -> -- page size of the references, as for 'binfoTxRefs'+  HashMap XPubSpec [XPubBal] -> -- xpub balances+  HashMap Address (Maybe BinfoXPubPath) -> -- address book+  HashSet XPubSpec -> -- show xpubs+  HashSet Address -> -- show addrs+  HashSet Address -> -- balance addresses+  BinfoFilter ->+  Bool -> -- numtxid+  Bool -> -- prune outputs+  Int64 -> -- starting balance+  ConduitT () BinfoTx IO ()+getBinfoTxs+  q+  pageSize+  xbals+  abook+  sxspecs+  saddrs+  baddrs+  bfilter+  numtxid+  prune+  bal =+    binfoTxRefs q pageSize xbals sxspecs saddrs .| bodies .| go bal+    where+      binfo_tx = toBinfoTx numtxid abook prune+      compute_bal_change t =+        let ins = map (.output) t.inputs+            out = t.outputs+            f b BinfoTxOutput {..} =+              let val = fromIntegral value+               in case address of+                    Nothing -> 0+                    Just a+                      | a `HashSet.member` baddrs ->+                          if b then val else negate val+                      | otherwise -> 0+         in sum $ map (f False) ins <> map (f True) out+      -- the bodies, read a page at a time, which is one round trip to Redis+      -- or one multi-get of the database instead of one per transaction+      bodies = do+        refs <- takeC (fromIntegral pageSize) .| sinkList+        unless (null refs) $ do+          lift (C.getTransactions q (map (.txid) refs)) >>= mapM_ yield+          bodies+      go b =+        await >>= \case+          Nothing -> return ()+          Just Nothing -> go b+          Just (Just x) -> do+            let a = binfo_tx b x+                b' = b - compute_bal_change a+                c = isJust a.blockHeight+                d = fst (fromJust a.balance)+                r = d + fromIntegral a.fee+            case bfilter of+              BinfoFilterAll ->+                yield a >> go b'+              BinfoFilterSent+                | 0 > r -> yield a >> go b'+                | otherwise -> go b'+              BinfoFilterReceived+                | r > 0 -> yield a >> go b'+                | otherwise -> go b'+              BinfoFilterMoved+                | r == 0 -> yield a >> go b'+                | otherwise -> go b'+              BinfoFilterConfirmed+                | c -> yield a >> go b'+                | otherwise -> go b'+              BinfoFilterMempool+                | c -> return ()+                | otherwise -> yield a >> go b'++-- | Page size to read the references of the transactions in, to show this+-- many after this offset: all of them in one page if they fit in 1000.+pageFor :: Int -> Int -> Word32+pageFor offset n = fromIntegral (max 16 (min 1000 (offset + n)))++-- | Whether to show addresses in CashAddr format (@cashaddr@).+getCashAddr :: Req -> IO Bool+getCashAddr req = paramOr req readBool "cashaddr" False++-- | Parse an address. Responds with not found if invalid.+getAddress :: WebState -> Text -> IO Address+getAddress st txt = do+  let net = st.config.store.net+  case textToAddr net txt of+    Nothing -> raise st ThingNotFound+    Just a -> return a++-- | Address or xpub in a path parameter. Passes to the next route if invalid.+getBinfoAddr :: WebState -> Req -> Text -> IO BinfoAddr+getBinfoAddr st req param' = do+  txt <- capture req param'+  let net = st.config.store.net+  let ctx = st.config.store.ctx+  let addr = BinfoAddr <$> textToAddr net txt+      xpub = BinfoXpub <$> xPubImport net ctx txt+  maybe next return (addr <|> xpub)++-- | Handler for @GET\/POST \/blockchain\/export-history@: daily history of the+-- balance of addresses and xpubs with prices, as Blockchain.info.+handleBinfoHistory :: WebState -> Req -> IO Response+handleBinfoHistory st req =+  withMetrics st (.binfoExportHistory) $ do+    (xspecs, addrs) <- getBinfoActive st req+    (startM, endM) <- get_dates+    (code, price') <- getPrice st req+    xbals <- (getXBals st.query xspecs)+    let xaddrs = HashSet.fromList $ concatMap (map get_addr) (HashMap.elems xbals)+        aaddrs = xaddrs <> addrs+        cur = price'.fifteen+        cs' = conduits (HashMap.toList xbals) addrs endM+    txs <-+      runConduit $+        joinDescStreams txRefKey cs'+          .| takeWhileC (is_newer startM)+          .| concatMapMC get_transaction+          .| sinkList+    let times = map (\Transaction {..} -> timestamp) txs+    let net = st.config.store.net+    let url = st.config.priceHistoryURL+    let session = st.session+    let logger = st.config.logger+    rates <- map (.price) <$> (getRates logger net session url code times)+    let hs = zipWith (convert cur aaddrs) txs (rates <> repeat 0.0)+    streamEncoding $ toEncoding hs+  where+    is_newer+      (Just BlockData {height = bh})+      TxRef {block = BlockRef {height = th}} =+        bh <= th+    is_newer Nothing TxRef {} = True+    is_newer _ _ = undefined+    get_addr = (.balance.address)+    get_transaction TxRef {txid = h} =+      C.getTransaction st.query h+    convert cur addrs tx rate =+      let ins = tx.inputs+          outs = tx.outputs+          fins = filter (input_addr addrs) ins+          fouts = filter (output_addr addrs) outs+          vin = fromIntegral . sum $ map (.value) fins+          vout = fromIntegral . sum $ map (.value) fouts+          fee = tx.fee+          v = vout - vin+          t = tx.timestamp+          h = txHash $ transactionData tx+       in toBinfoHistory v t rate cur fee h+    input_addr addrs' StoreInput {address = Just a} =+      a `HashSet.member` addrs'+    input_addr _ _ = False+    output_addr addrs' StoreOutput {address = Just a} =+      a `HashSet.member` addrs'+    output_addr _ _ = False+    get_dates = do+      BinfoDate start <- requireParam st req parseBinfoDate "start"+      BinfoDate end' <- requireParam st req parseBinfoDate "end"+      let end = end' + 24 * 60 * 60+      let ch = st.config.store.chain+      startM <- C.blockAtOrAfter st.query ch start+      endM <- C.blockAtOrBefore st.query ch end+      return (startM, endM)+    conduits xpubs addrs endM =+      map (uncurry (xpub_c endM)) xpubs+        <> map (addr_c endM) (HashSet.toList addrs)+    lim endM =+      Limits+        { limit = 16,+          offset = 0,+          start = AtBlock . (.height) <$> endM+        }+    addr_c endM a =+      streamThings (st.query.getAddressTxs a) (Just (.txid)) (lim endM)+    xpub_c endM x bs =+      streamThings (st.query.xPubTxs x bs) (Just (.txid)) (lim endM)++-- | Currency code in the @currency@ parameter, USD by default, in any case,+-- and its latest price. A currency that the price service does not list is+-- refused. Until the server has prices, which it may never have if the+-- service cannot be reached, every currency has a price of zero, rather than+-- every request being refused.+getPrice :: WebState -> Req -> IO (Text, BinfoTicker)+getPrice st req = do+  prices <- readTVarIO st.ticker+  let known c+        | HashMap.null prices = Just (c, def)+        | otherwise = (c,) <$> HashMap.lookup c prices+  paramOr req (known . T.toUpper) "currency" ("USD", fromMaybe def (HashMap.lookup "USD" prices))++-- | Symbol of the currency in the @currency@ parameter.+getSymbol :: WebState -> Req -> IO BinfoSymbol+getSymbol st req = uncurry binfoTickerToSymbol <$> getPrice st req++-- | Handler for @GET \/blockchain\/blocks\/:milliseconds@: the blocks of the day+-- before a time in milliseconds, as Blockchain.info.+handleBinfoBlocksDay :: WebState -> Req -> IO Response+handleBinfoBlocksDay st req =+  withMetrics st (.binfoBlock) $ do+    t <- min h . (`div` 1000) <$> captureWith req readNum "milliseconds"+    let ch = st.config.store.chain+    m <- C.blockAtOrBefore st.query ch t+    bs <- go (d t) m+    streamEncoding $ toEncoding $ map toBinfoBlockInfo bs+  where+    h = fromIntegral (maxBound :: H.Timestamp)+    d = subtract (24 * 3600)+    go _ Nothing = return []+    go t (Just b)+      | b.header.timestamp <= fromIntegral t =+          return []+      | otherwise = do+          b' <- st.query.getBlock b.header.prev+          (b :) <$> go t b'++-- | Handler for @GET\/POST \/blockchain\/multiaddr@: balances and transactions of+-- addresses and xpubs, as Blockchain.info.+handleMultiAddr :: WebState -> Req -> IO Response+handleMultiAddr st req =+  withMetrics st (.binfoMultiaddr) $ do+    (addrs', _, saddrs, sxpubs, xspecs) <- get_addrs+    numtxid <- getNumTxId req+    cashaddr <- getCashAddr req+    local' <- getSymbol st req+    offset <- getBinfoOffset st req+    n <- getBinfoCount st req "n"+    prune <- get_prune+    fltr <- get_filter+    xbals <- (getXBals st.query xspecs)+    xtxns <- get_xpub_tx_count xbals xspecs+    let sxbals = only_show_xbals sxpubs xbals+        xabals = compute_xabals xbals+        addrs = addrs' `HashSet.difference` HashMap.keysSet xabals+    abals <- get_abals addrs+    let sxspecs = only_show_xspecs sxpubs xspecs+        sxabals = compute_xabals sxbals+        sabals = only_show_abals saddrs abals+        sallbals = sabals <> sxabals+        sbal = compute_bal sallbals+        allbals = abals <> xabals+        abook = compute_abook addrs xbals+        sxaddrs = compute_xaddrs sxbals+        salladdrs = saddrs <> sxaddrs+        bal = compute_bal allbals+        ibal = fromIntegral sbal+    ftxs <-+      runConduit $+        getBinfoTxs+          st.query+          (pageFor offset n)+          xbals+          abook+          sxspecs+          saddrs+          salladdrs+          fltr+          numtxid+          prune+          ibal+          .| (dropC offset >> takeC n .| sinkList)+    -- counted without reading the transactions, which takes seconds for+    -- xpubs with tens of thousands of them: one xpub has its own count, from+    -- the cache when it is cached; for several xpubs and addresses, the+    -- database counts the distinct transactions of all their addresses+    ntx <- case (HashSet.toList sxspecs, HashSet.null saddrs) of+      ([x], True) -> return (HashMap.findWithDefault 0 x xtxns)+      _ -> fromIntegral <$> st.direct.getAddressesTxCount (HashSet.toList salladdrs)+    nfiltered <- case fltr of+      BinfoFilterAll -> return ntx+      _ ->+        runConduit $+          getBinfoTxs+            st.query+            1000+            xbals+            abook+            sxspecs+            saddrs+            salladdrs+            fltr+            numtxid+            prune+            ibal+            .| lengthC+    let net = st.config.store.net+    let ctx = st.config.store.ctx+    best <- get_best_block+    peers <- get_peers+    let baddrs = toBinfoAddrs sabals sxbals xtxns+        abaddrs = toBinfoAddrs abals xbals xtxns+        recv = sum $ map (.received) abaddrs+        sent' = sum $ map (.sent) abaddrs+        wallet =+          BinfoWallet+            { balance = bal,+              txs = ntx,+              filtered = nfiltered,+              received = recv,+              sent = sent'+            }+        coin = netBinfoSymbol net+    let block =+          BinfoBlockInfo+            { hash = H.headerHash best.header,+              height = best.height,+              timestamp = best.header.timestamp,+              index = best.height+            }+    let info =+          BinfoInfo+            { connected = peers,+              conversion = 100 * 1000 * 1000,+              fiat = local',+              crypto = coin,+              head = block+            }+    let multiaddr =+          BinfoMultiAddr+            { addresses = baddrs,+              wallet = wallet,+              txs = ftxs,+              info = info,+              recommendFee = True,+              cashAddr = cashaddr+            }+    streamEncoding $ marshalEncoding (net, ctx) multiaddr+  where+    get_xpub_tx_count xbals =+      fmap HashMap.fromList+        . mapM+          ( \x ->+              (x,)+                . fromIntegral+                <$> st.query.xPubTxCount x (xBals x xbals)+          )+        . HashSet.toList+    get_filter = paramOr req parseBinfoFilter "filter" BinfoFilterAll+    get_best_block =+      st.query.getBestBlock >>= \case+        Nothing -> raise st ThingNotFound+        Just bh ->+          st.query.getBlock bh >>= \case+            Nothing -> raise st ThingNotFound+            Just b -> return b+    get_prune = do+      noCompact <- paramOr req readBool "no_compact" False+      not <$> paramOr req readBool "noCompact" noCompact+    only_show_xbals sxpubs =+      HashMap.filterWithKey (\k _ -> k.key `HashSet.member` sxpubs)+    only_show_xspecs sxpubs =+      HashSet.filter (\k -> k.key `HashSet.member` sxpubs)+    only_show_abals saddrs =+      HashMap.filterWithKey (\k _ -> k `HashSet.member` saddrs)+    addr (BinfoAddr a) = Just a+    addr (BinfoXpub _) = Nothing+    xpub (BinfoXpub x) = Just x+    xpub (BinfoAddr _) = Nothing+    get_addrs = do+      (xspecs, addrs) <- getBinfoActive st req+      sh <- getBinfoAddrsParam st req "onlyShow"+      let xpubs = HashSet.map (.key) xspecs+          actives =+            HashSet.map BinfoAddr addrs+              <> HashSet.map BinfoXpub xpubs+          sh' = if HashSet.null sh then actives else sh+          saddrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList sh'+          sxpubs = HashSet.fromList $ mapMaybe xpub $ HashSet.toList sh'+      return (addrs, xpubs, saddrs, sxpubs, xspecs)+    get_abals =+      let f b = (b.address, b)+          g = HashMap.fromList . map f+       in fmap g . st.query.getBalances . HashSet.toList+    get_peers = do+      ps <- getPeersInformation st.config.store.peerMgr+      return (fromIntegral (length ps))+    compute_xabals =+      let f b = (b.balance.address, b.balance)+       in HashMap.fromList . concatMap (map f) . HashMap.elems+    compute_bal =+      let f b = b.confirmed + b.unconfirmed+       in sum . map f . HashMap.elems+    compute_abook addrs xbals =+      let f xs xb =+            let a = xb.balance.address+                e = error "lions and tigers and bears"+                s = toSoft (listToPath xb.path)+             in (a, Just (BinfoXPubPath xs.key (fromMaybe e s)))+          amap =+            HashMap.map (const Nothing) $+              HashSet.toMap addrs+          xmap =+            HashMap.fromList+              . concatMap (uncurry (map . f))+              $ HashMap.toList xbals+       in amap <> xmap+    compute_xaddrs =+      let f = map (.balance.address)+       in HashSet.fromList . concatMap f . HashMap.elems++-- | Number of items requested in a parameter, within the limits.+getBinfoCount :: WebState -> Req -> Text -> IO Int+getBinfoCount st req str = do+  let d = st.config.limits.defItemCount+  let x = st.config.limits.maxFullItemCount+  i <- fromIntegral <$> paramOr req readNat str (fromIntegral d)+  return (fromIntegral (if x == 0 then i else min x i) :: Int)++-- | Offset requested in the @offset@ parameter. Responds with an error if above the maximum.+getBinfoOffset ::+  WebState ->+  Req ->+  IO Int+getBinfoOffset st req = do+  let x = st.config.limits.maxOffset+  o <- fromIntegral <$> paramOr req readNat "offset" 0+  when (o > x) $+    raise st $+      UserError $+        "offset exceeded: " <> show o <> " > " <> show x+  return (fromIntegral o :: Int)++-- | Handler for @GET \/blockchain\/rawaddr\/:addr@: balance and transactions of an+-- address or xpub, as Blockchain.info; also at @\/blockchain\/address\/:addr@+-- and @\/blockchain\/xpub\/:addr@.+handleRawAddr :: WebState -> Req -> IO Response+handleRawAddr st req =+  withMetrics st (.binfoAddressRaw) $+    getBinfoAddr st req "addr" >>= \case+      BinfoAddr addr -> do_addr addr+      BinfoXpub xpub -> do_xpub xpub+  where+    do_xpub xpub = do+      numtxid <- getNumTxId req+      derive <- paramOr req (readDerive st.config.store.net) "derive" DeriveNormal+      let xspec = XPubSpec xpub derive+      n <- getBinfoCount st req "limit"+      off <- getBinfoOffset st req+      xbals <- (getXBals st.query (HashSet.singleton xspec))+      checkDerive st xspec (xBals xspec xbals)+      let net = st.config.store.net+      let summary = xPubSummary xspec (xBals xspec xbals)+          abook = compute_abook xpub (xBals xspec xbals)+          xspecs = HashSet.singleton xspec+          saddrs = HashSet.empty+          baddrs = HashMap.keysSet abook+          bfilter = BinfoFilterAll+          amnt = summary.confirmed + summary.unconfirmed+      txs <-+        runConduit $+          getBinfoTxs+            st.query+            (pageFor off n)+            xbals+            abook+            xspecs+            saddrs+            baddrs+            bfilter+            numtxid+            False+            (fromIntegral amnt)+            .| (dropC off >> takeC n .| sinkList)+      -- n_tx is every transaction of the xpub, as for an address, not the+      -- number on this page+      ntx <- st.query.xPubTxCount xspec (xBals xspec xbals)+      let ra =+            BinfoRawAddr+              { address = BinfoXpub xpub,+                balance = amnt,+                ntx = fromIntegral ntx,+                utxo = summary.utxo,+                received = summary.received,+                sent = fromIntegral summary.received - fromIntegral amnt,+                txs = txs+              }+      let ctx = st.config.store.ctx+      streamEncoding $ marshalEncoding (net, ctx) ra+    compute_abook xpub xbals =+      let f xb =+            let a = xb.balance.address+                e = error "black hole swallows all your code"+                s = toSoft $ listToPath xb.path+                m = fromMaybe e s+             in (a, Just (BinfoXPubPath xpub m))+       in HashMap.fromList $ map f xbals+    do_addr addr = do+      numtxid <- getNumTxId req+      n <- getBinfoCount st req "limit"+      off <- getBinfoOffset st req+      bal <- fromMaybe (zeroBalance addr) <$> st.query.getBalance addr+      let net = st.config.store.net+      let abook = HashMap.singleton addr Nothing+          xspecs = HashSet.empty+          saddrs = HashSet.singleton addr+          bfilter = BinfoFilterAll+          balance = bal.confirmed + bal.unconfirmed+      txs <-+        runConduit $+          getBinfoTxs+            st.query+            (pageFor off n)+            HashMap.empty+            abook+            xspecs+            saddrs+            saddrs+            bfilter+            numtxid+            False+            (fromIntegral balance)+            .| (dropC off >> takeC n .| sinkList)+      let ra =+            BinfoRawAddr+              { address = BinfoAddr addr,+                balance,+                ntx = bal.txs,+                utxo = bal.utxo,+                received = bal.received,+                sent = fromIntegral bal.received - fromIntegral balance,+                txs+              }+      let ctx = st.config.store.ctx+      streamEncoding $ marshalEncoding (net, ctx) ra++-- | Handler for @GET \/blockchain\/q\/getreceivedbyaddress\/:addr@: total received by an address.+handleBinfoReceived :: WebState -> Req -> IO Response+handleBinfoReceived st req =+  withMetrics st (.binfoQgetreceivedbyaddress) $ do+    a <- getAddress st =<< capture req "addr"+    b <- fromMaybe (zeroBalance a) <$> st.query.getBalance a+    textResponse $ cs $ show b.received++-- | Handler for @GET \/blockchain\/q\/getsentbyaddress\/:addr@: total sent by an address.+handleBinfoSent :: WebState -> Req -> IO Response+handleBinfoSent st req =+  withMetrics st (.binfoQgetsentbyaddress) $ do+    a <- getAddress st =<< capture req "addr"+    b <- fromMaybe (zeroBalance a) <$> st.query.getBalance a+    textResponse $ cs $ show $ b.received - b.confirmed - b.unconfirmed++-- | Handler for @GET \/blockchain\/q\/addressbalance\/:addr@: balance of an address.+handleBinfoAddrBalance :: WebState -> Req -> IO Response+handleBinfoAddrBalance st req =+  withMetrics st (.binfoQaddressbalance) $ do+    a <- getAddress st =<< capture req "addr"+    b <- fromMaybe (zeroBalance a) <$> st.query.getBalance a+    textResponse $ cs $ show $ b.confirmed + b.unconfirmed++-- | Handler for @GET \/blockchain\/q\/addressfirstseen\/:addr@: time of the first+-- block with a transaction of an address.+handleFirstSeen :: WebState -> Req -> IO Response+handleFirstSeen st req =+  withMetrics st (.binfoQaddressfirstseen) $ do+    a <- getAddress st =<< capture req "addr"+    let ch = st.config.store.chain+    bb <- chainGetBest ch+    let top = bb.height+        bot = 0+    i <- go ch bb a bot top+    textResponse $ cs $ show i+  where+    go ch bb a bot top = do+      let mid = bot + (top - bot) `div` 2+          n = top - bot < 2+      x <- hasone a bot+      y <- hasone a mid+      z <- hasone a top+      if+        | x -> getblocktime ch bb bot+        | n -> getblocktime ch bb top+        | y -> go ch bb a bot mid+        | z -> go ch bb a mid top+        | otherwise -> return 0+    getblocktime ch bb h =+      (chainGetAncestor ch h bb) >>= \case+        Just b -> return b.header.timestamp+        Nothing -> do+          let logger = st.config.logger+          logError logger $+            "Could not get ancestor at height "+              <> cs (show h)+              <> " for block "+              <> H.blockHashToHex (H.headerHash bb.header)+          error "Block ancestor retreival error"+    hasone a h = do+      let l = Limits 1 0 (Just (AtBlock h))+      not . null <$> st.query.getAddressTxs a l++-- | Handler for @GET\/POST \/blockchain\/balance@: short balances of addresses and xpubs, as Blockchain.info.+handleShortBal :: WebState -> Req -> IO Response+handleShortBal st req =+  withMetrics st (.binfoBalance) $ do+    (xspecs, addrs) <- getBinfoActive st req+    cashaddr <- getCashAddr req+    let net = st.config.store.net+    abals <- catMaybes <$> mapM (getabal net cashaddr) (HashSet.toList addrs)+    xbals <- mapM (getxbal net) (HashSet.toList xspecs)+    let res = HashMap.fromList (abals <> xbals)+    streamEncoding $ toEncoding res+  where+    shorten bal =+      BinfoShortBal+        { final = bal.confirmed + bal.unconfirmed,+          ntx = bal.txs,+          received = bal.received+        }+    getabal net cashaddr a =+      let net' =+            if+              | cashaddr -> net+              | net == bch -> btc+              | net == bchTest -> btcTest+              | net == bchTest4 -> btcTest+              | otherwise -> net+       in case addrToText net' a of+            Nothing -> return Nothing+            Just a' ->+              st.query.getBalance a >>= \case+                Nothing -> return $ Just (a', shorten (zeroBalance a))+                Just b -> return $ Just (a', shorten b)+    getxbal net xpub = do+      xbals <- st.query.xPubBals xpub+      ntx <- fromIntegral <$> st.query.xPubTxCount xpub xbals+      let val = sum $ map (.balance.confirmed) xbals+          zro = sum $ map (.balance.unconfirmed) xbals+          exs = [x | x@XPubBal {path = 0 : _} <- xbals]+          final = val + zro+          received = sum $ map (.balance.received) exs+          sbl = BinfoShortBal {final, ntx, received}+      let ctx = st.config.store.ctx+      return (xPubExport net ctx xpub.key, sbl)++-- | Whether to respond with hex (@format=hex@).+getBinfoHex :: Req -> IO Bool+getBinfoHex req = paramOr req format "format" False+  where+    format "hex" = Just True+    format "json" = Just False+    format _ = Nothing++-- | Handler for @GET \/blockchain\/block-height\/:height@: the blocks at a height, as Blockchain.info.+handleBinfoBlockHeight :: WebState -> Req -> IO Response+handleBinfoBlockHeight st req =+  withMetrics st (.binfoBlockHeight) $ do+    numtxid <- getNumTxId req+    height <- captureWith req readNum "height"+    bs <- fmap catMaybes $ st.query.getBlocksAtHeight height >>= mapM (st.query.getBlock)+    ns <- fmap catMaybes $ st.query.getBlocksAtHeight (height + 1) >>= mapM (st.query.getBlock)+    is <- mapM (get_binfo_blocks numtxid ns) bs+    let net = st.config.store.net+    let ctx = st.config.store.ctx+    streamEncoding $ marshalEncoding (net, ctx) is+  where+    get_binfo_blocks numtxid ns b = do+      txs <- catMaybes <$> C.getTransactions st.query b.txs+      let h x = H.headerHash x.header+          nbs = [h n | n <- ns, n.header.prev == h b]+          bts = map (toBinfoTxSimple numtxid) txs+      return $ toBinfoBlock b bts nbs++-- | Handler for @GET \/blockchain\/latestblock@: the best block, as Blockchain.info.+handleBinfoLatest :: WebState -> Req -> IO Response+handleBinfoLatest st req =+  withMetrics st (.binfoBlockLatest) $ do+    numtxid <- getNumTxId req+    b <- (bestBlock st.query) >>= maybe (raise st ThingNotFound) return+    streamEncoding $+      toEncoding+        BinfoHeader+          { hash = H.headerHash b.header,+            timestamp = b.header.timestamp,+            index = b.height,+            height = b.height,+            txids = map (encodeBinfoTxId numtxid) b.txs+          }++-- | Handler for @GET \/blockchain\/rawblock\/:block@: a block with its transactions, as Blockchain.info.+handleBinfoBlock :: WebState -> Req -> IO Response+handleBinfoBlock st req =+  withMetrics st (.binfoBlockRaw) $ do+    numtxid <- getNumTxId req+    hex <- getBinfoHex req+    captureWith req parseBinfoBlockId "block" >>= \case+      BinfoBlockHash bh -> go numtxid hex bh+      BinfoBlockIndex i ->+        st.query.getBlocksAtHeight i >>= \case+          [] -> raise st ThingNotFound+          bh : _ -> go numtxid hex bh+  where+    go numtxid hex bh = do+      b <- maybe (raise st ThingNotFound) return =<< st.query.getBlock bh+      txs <- catMaybes <$> C.getTransactions st.query b.txs+      nhs <- fmap catMaybes $ st.query.getBlocksAtHeight (b.height + 1) >>= mapM (st.query.getBlock)+      let h x = H.headerHash x.header+          nxt = [h n | n <- nhs, n.header.prev == h b]+      if hex+        then do+          let x = H.Block b.header (map transactionData txs)+          textResponse . encodeHexLazy . runPutL $ serialize x+        else do+          let bts = map (toBinfoTxSimple numtxid) txs+              y = toBinfoBlock b bts nxt+          let net = st.config.store.net+          let ctx = st.config.store.ctx+          streamEncoding $ marshalEncoding (net, ctx) y++-- | Transaction by hash or short number. Fails with 'TxIndexConflict' if a short number matches several.+getBinfoTx ::+  WebState ->+  BinfoTxId ->+  IO (Either Except Transaction)+getBinfoTx st txid = do+  tx <- case txid of+    BinfoTxIdHash h -> maybeToList <$> C.getTransaction st.query h+    BinfoTxIdIndex i -> C.getNumTransaction st.query i+  case tx of+    [t] -> return $ Right t+    [] -> return $ Left ThingNotFound+    ts ->+      let tids = map (txHash . transactionData) ts+       in return $ Left (TxIndexConflict tids)++-- | Handler for @GET \/blockchain\/rawtx\/:txid@: a transaction, as Blockchain.info.+handleBinfoTx :: WebState -> Req -> IO Response+handleBinfoTx st req =+  withMetrics st (.binfoTxRaw) $ do+    numtxid <- getNumTxId req+    hex <- getBinfoHex req+    txid <- captureWith req parseBinfoTxId "txid"+    tx <-+      getBinfoTx st txid >>= \case+        Right t -> return t+        Left e -> raise st e+    if hex then hx tx else js numtxid tx+  where+    js numtxid t = do+      let net = st.config.store.net+      let ctx = st.config.store.ctx+      streamEncoding $ marshalEncoding (net, ctx) $ toBinfoTxSimple numtxid t+    hx t = do+      textResponse . encodeHexLazy . runPutL . serialize $ transactionData t++-- | Handler for @GET \/blockchain\/q\/txtotalbtcoutput\/:txid@: total value of the outputs of a transaction.+handleBinfoTotalOut :: WebState -> Req -> IO Response+handleBinfoTotalOut st req =+  withMetrics st (.binfoQtxtotalbtcoutput) $ do+    txid <- captureWith req parseBinfoTxId "txid"+    tx <-+      getBinfoTx st txid >>= \case+        Right t -> return t+        Left e -> raise st e+    textResponse $ cs $ show $ sum $ map (.value) tx.outputs++-- | Handler for @GET \/blockchain\/q\/txfee\/:txid@: fee of a transaction.+handleBinfoTxFees :: WebState -> Req -> IO Response+handleBinfoTxFees st req =+  withMetrics st (.binfoQtxfee) $ do+    txid <- captureWith req parseBinfoTxId "txid"+    tx <-+      getBinfoTx st txid >>= \case+        Right t -> return t+        Left e -> raise st e+    let i = sum $ map (.value) $ filter f tx.inputs+        o = sum $ map (.value) tx.outputs+    textResponse . cs . show $ i - o+  where+    f StoreInput {} = True+    f StoreCoinbase {} = False++-- | Handler for @GET \/blockchain\/q\/txresult\/:txid\/:addr@: net amount that a transaction sends to an address.+handleBinfoTxResult :: WebState -> Req -> IO Response+handleBinfoTxResult st req =+  withMetrics st (.binfoQtxresult) $ do+    txid <- captureWith req parseBinfoTxId "txid"+    addr <- getAddress st =<< capture req "addr"+    tx <-+      getBinfoTx st txid >>= \case+        Right t -> return t+        Left e -> raise st e+    let i = toInteger $ sum $ map (.value) $ filter (f addr) tx.inputs+        o = toInteger $ sum $ map (.value) $ filter (g addr) tx.outputs+    textResponse $ cs $ show $ o - i+  where+    f addr StoreInput {address = Just a} = a == addr+    f _ _ = False+    g addr StoreOutput {address = Just a} = a == addr+    g _ _ = False++-- | Handler for @GET \/blockchain\/q\/txtotalbtcinput\/:txid@: total value of the inputs of a transaction.+handleBinfoTotalInput :: WebState -> Req -> IO Response+handleBinfoTotalInput st req =+  withMetrics st (.binfoQtxtotalbtcinput) $ do+    txid <- captureWith req parseBinfoTxId "txid"+    tx <-+      getBinfoTx st txid >>= \case+        Right t -> return t+        Left e -> raise st e+    textResponse $ cs $ show $ sum $ map (.value) $ filter f $ tx.inputs+  where+    f StoreInput {} = True+    f StoreCoinbase {} = False++-- | Handler for @GET \/blockchain\/unconfirmed-transactions@: mempool transactions, as Blockchain.info.+handleBinfoMempool :: WebState -> Req -> IO Response+handleBinfoMempool st req =+  withMetrics st (.binfoMempool) $ do+    numtxid <- getNumTxId req+    offset <- getBinfoOffset st req+    n <- getBinfoCount st req "limit"+    -- a limit of 0 would mean no limit to st.query.getMempoolPage+    txids <-+      if n <= 0+        then return []+        else+          map snd+            <$> st.query.getMempoolPage (Limits (fromIntegral n) (fromIntegral offset) Nothing)+    txs <- catMaybes <$> C.getTransactions st.query txids+    let net = st.config.store.net+    let mem = BinfoMempool $ map (toBinfoTxSimple numtxid) txs+    let ctx = st.config.store.ctx+    streamEncoding $ marshalEncoding (net, ctx) mem++-- | Handler for @GET \/blockchain\/q\/getblockcount@: height of the best header.+handleBinfoGetBlockCount :: WebState -> Req -> IO Response+handleBinfoGetBlockCount st _ =+  withMetrics st (.binfoQgetblockcount) $ do+    let ch = st.config.store.chain+    bn <- chainGetBest ch+    textResponse $ TL.pack $ show bn.height++-- | Handler for @GET \/blockchain\/q\/latesthash@: hash of the best header.+handleBinfoLatestHash :: WebState -> Req -> IO Response+handleBinfoLatestHash st _ =+  withMetrics st (.binfoQlatesthash) $ do+    let ch = st.config.store.chain+    bn <- chainGetBest ch+    textResponse $ TL.fromStrict $ H.blockHashToHex $ H.headerHash bn.header++-- | Handler for @GET \/blockchain\/q\/bcperblock@: block subsidy at the next height.+handleBinfoSubsidy :: WebState -> Req -> IO Response+handleBinfoSubsidy st _ =+  withMetrics st (.binfoQbcperblock) $ do+    let ch = st.config.store.chain+    let net = st.config.store.net+    bn <- chainGetBest ch+    textResponse $+      cs $+        show $+          (/ (100 * 1000 * 1000 :: Double)) $+            fromIntegral $+              H.computeSubsidy net (bn.height + 1)++-- | Handler for @GET \/blockchain\/q\/addresstohash\/:addr@: hash160 of an address in hex.+handleBinfoAddrToHash :: WebState -> Req -> IO Response+handleBinfoAddrToHash st req =+  withMetrics st (.binfoQaddresstohash) $ do+    addr <- getAddress st =<< capture req "addr"+    if+      | Just h <- addressHash160 addr ->+          textResponse $ encodeHexLazy $ runPutL $ serialize h+      | Just h <- addressHash256 addr ->+          textResponse $ encodeHexLazy $ runPutL $ serialize h+      | otherwise ->+          textResponse ""++-- | Handler for @GET \/blockchain\/q\/hashtoaddress\/:hash@: pay-to-public-key-hash address of a hash160.+handleBinfoHashToAddr :: WebState -> Req -> IO Response+handleBinfoHashToAddr st req =+  withMetrics st (.binfoQhashtoaddress) $ do+    bs <- maybe next return . decodeHex =<< capture req "hash"+    let net = st.config.store.net+    hash <- either (const next) return $ decode bs+    addr <- maybe next return $ addrToText net $ PubKeyAddress hash+    textResponse $ TL.fromStrict addr++-- | Handler for @GET \/blockchain\/q\/addrpubkey\/:pubkey@: address of a public key.+handleBinfoAddrPubkey :: WebState -> Req -> IO Response+handleBinfoAddrPubkey st req =+  withMetrics st (.binfoQaddrpubkey) $ do+    hex <- capture req "pubkey"+    let ctx = st.config.store.ctx+    pubkey <-+      maybe next (return . pubKeyAddr ctx) $+        eitherToMaybe . unmarshal ctx =<< decodeHex hex+    let net = st.config.store.net+    case addrToText net pubkey of+      Nothing -> raise st ThingNotFound+      Just a -> textResponse $ TL.fromStrict a++-- | Handler for @GET \/blockchain\/q\/pubkeyaddr\/:addr@: public key of an+-- address, found in a transaction input that spends from it.+handleBinfoPubKeyAddr :: WebState -> Req -> IO Response+handleBinfoPubKeyAddr st req =+  withMetrics st (.binfoQpubkeyaddr) $ do+    addr <- getAddress st =<< capture req "addr"+    mi <- strm addr+    i <- case mi of+      Nothing -> raise st ThingNotFound+      Just i -> return i+    pk <- case extr addr i of+      Left e -> raise st $ UserError e+      Right t -> return t+    textResponse $ encodeHexLazy $ L.fromStrict pk+  where+    strm addr = do+      runConduit $ do+        let l = Limits {limit = 8, offset = 0, start = Nothing}+        streamThings (st.query.getAddressTxs addr) (Just (.txid)) l+          .| concatMapMC (C.getTransaction st.query . (.txid))+          .| concatMapC (filter (inp addr) . (.inputs))+          .| headC+    inp addr StoreInput {address = Just a} = a == addr+    inp _ _ = False+    extr addr StoreInput {script, pkscript, witness} = do+      Script sig <- decode script+      Script pks <- decode pkscript+      case addr of+        PubKeyAddress {} ->+          case sig of+            [OP_PUSHDATA _ _, OP_PUSHDATA pub _] ->+              Right pub+            [OP_PUSHDATA _ _] ->+              case pks of+                [OP_PUSHDATA pub _, OP_CHECKSIG] ->+                  Right pub+                _ -> Left "Could not parse scriptPubKey"+            _ -> Left "Could not parse scriptSig"+        WitnessPubKeyAddress {} ->+          case witness of+            [_, pub] -> return pub+            _ -> Left "Could not parse scriptPubKey"+        _ -> Left "Address does not have public key"+    extr _ _ = Left "Incorrect input type"++-- | Handler for @GET \/blockchain\/q\/hashpubkey\/:pubkey@: hash160 of a public key in hex.+handleBinfoHashPubkey :: WebState -> Req -> IO Response+handleBinfoHashPubkey st req =+  withMetrics st (.binfoQhashpubkey) $ do+    let ctx = st.config.store.ctx+    pkm <-+      fmap+        (eitherToMaybe . unmarshal ctx <=< decodeHex)+        (capture req "pubkey")+    addr <- case pkm of+      Nothing -> raise st $ UserError "Could not decode public key"+      Just pk -> return $ pubKeyAddr ctx pk+    if+      | Just h <- addressHash160 addr ->+          textResponse $ encodeHexLazy $ runPutL $ serialize h+      | Just h <- addressHash256 addr ->+          textResponse $ encodeHexLazy $ runPutL $ serialize h+      | otherwise ->+          textResponse ""++-- GET Network Information --++-- | Handler for @GET \/peers@: connected peers.+handlePeers ::+  WebState ->+  GetPeers ->+  IO [PeerInfo]+handlePeers st _ =+  withMetrics st (.peers) $ do+    getPeersInformation =<< pure st.config.store.peerMgr++-- | Obtain information about connected peers from peer manager process.+getPeersInformation ::+  PeerMgr -> IO [PeerInfo]+getPeersInformation mgr =+  mapMaybe toInfo <$> (getPeers mgr)+  where+    toInfo op = do+      ver <- op.version+      return+        PeerInfo+          { userAgent = ver.userAgent.get,+            address = show op.address,+            -- Signed, as the peer sent it. See 'PeerInfo'.+            version = ver.version,+            services = ver.services,+            relay = ver.relay+          }++-- | The route of @GET \/health@. Its status says in one number what a load+-- balancer needs to know:+--+-- * 200: every check passes;+--+-- * 203: every check passes, but Redis does not answer, so the store+--   answers from its own database and mempool alone;+--+-- * 503: a check fails.+--+-- = Why 203 for a store without its Redis+--+-- Such a store is not broken: its answers are right for what it has seen. A+-- load balancer should go on sending it requests, which it does for any+-- status in the 200s, so the status cannot be an error. But the answers are+-- no longer those of the fleet: they do not come from the copy that the+-- stores share. That is what 203, Non-Authoritative Information, says of a+-- response, and a monitor that expects exactly 200 will notice it. 207 was+-- the other candidate, for a body with a status for each part; it promises+-- a format that this body does not have. The response also names what is+-- missing in a @Haskoin-Degraded@ header.+--+-- = The check of Redis in the body+--+-- The body of a store that has a cache has one more check than the others,+-- 'Haskoin.Store.Data.redis', which says whether Redis answers. It does not+-- count towards the @ok@ of the whole, which stays the answer to "is this+-- store fit to serve".+pathHealth :: WebState -> Route+pathHealth st =+  route (renderStdMethod (resourceMethod proxy)) (capturePath proxy) ["pretty"] $ \req -> do+    proto <- serialAs req False+    h <- handleHealth st GetHealth+    let (status, extra)+          | not (isOK h) = (status503, [("Retry-After", "5")])+          | h.redis == Just False = (status203, [("Haskoin-Degraded", "redis")])+          | otherwise = (status200, [])+    return $+      responseLBS status ((hContentType, serialType proto) : extra) (protoSerial proto toEncoding toJSON h)+  where+    proxy = Proxy :: Proxy GetHealth++-- | Handler for @GET \/health@: health of the store.+handleHealth ::+  WebState ->+  GetHealth ->+  IO HealthCheck+handleHealth st _ =+  withMetrics st (.health) $ do+    h <- readTVarIO st.health+    up <- readTVarIO st.cacheUp+    -- a store without a cache has no Redis to be without+    let HealthCheck {blocks, lastBlock, lastTx, pendingTxs, peers, network, version, time} = h+    let shares = void st.config.store.cache <|> void st.config.store.fleet+    return HealthCheck {redis = up <$ shares, ..}++-- | Heights of the best header and best block, and the maximum lag allowed.+blockHealthCheck :: WebConfig -> IO BlockHealth+blockHealthCheck cfg = do+  let ch = cfg.store.chain+      db = cfg.store.db+  headers <- (.height) <$> chainGetBest ch+  blocks <-+    Q.getBestBlock db >>= \case+      Nothing -> return 0+      Just h -> maybe 0 (.height) <$> Q.getBlock db h+  return+    BlockHealth+      { headers,+        blocks,+        max = fromIntegral cfg.maxLaggingBlocks+      }++-- | Age of the best header and the maximum allowed.+lastBlockHealthCheck :: Chain -> WebLimits -> IO TimeHealth+lastBlockHealthCheck ch WebLimits {blockTimeout} = do+  n <- round <$> getPOSIXTime+  t <- fromIntegral . (.header.timestamp) <$> (chainGetBest ch)+  return+    TimeHealth+      { age = n - t,+        max = fromIntegral blockTimeout+      }++-- | Age of the newest mempool transaction or best header, and the maximum allowed.+lastTxHealthCheck :: WebConfig -> IO TimeHealth+lastTxHealthCheck WebConfig {noMempool, store = store', limits} = do+  n <- round <$> getPOSIXTime+  b <- fromIntegral . (.header.timestamp) <$> (chainGetBest ch)+  t <-+    Q.getMempoolPage store'.db (Limits 1 0 Nothing) >>= \case+      t : _ ->+        let x = fromIntegral $ fst t+         in return $ max x b+      [] -> return b+  return+    TimeHealth+      { age = n - t,+        max = fromIntegral to+      }+  where+    ch = store'.chain+    to =+      if noMempool+        then limits.blockTimeout+        else limits.txTimeout++-- | Transactions waiting to be imported and the maximum allowed.+pendingTxsHealthCheck :: WebConfig -> IO MaxHealth+pendingTxsHealthCheck cfg = do+  n <- blockStorePendingTxs cfg.store.block+  return+    MaxHealth+      { max = fromIntegral cfg.maxPendingTxs,+        count = fromIntegral n+      }++-- | Online peers and the minimum required.+peerHealthCheck :: WebConfig -> IO CountHealth+peerHealthCheck cfg = do+  count <- fromIntegral . length <$> (getPeers cfg.store.peerMgr)+  return CountHealth {min = fromIntegral cfg.minPeers, count}++-- | Run every health check.+healthCheck :: WebConfig -> IO HealthCheck+healthCheck cfg = do+  blocks <- blockHealthCheck cfg+  lastBlock <- lastBlockHealthCheck cfg.store.chain cfg.limits+  lastTx <- lastTxHealthCheck cfg+  pendingTxs <- pendingTxsHealthCheck cfg+  peers <- peerHealthCheck cfg+  time <- round <$> getPOSIXTime+  return+    HealthCheck+      { network = cfg.store.net.name,+        version = cfg.version,+        -- filled in when the check is served, from what the last request+        -- or ping found+        redis = Nothing,+        ..+      }++-- | Handler for @GET \/dbstats@: RocksDB memory use and statistics, as text.+handleDbStats :: WebState -> Req -> IO Response+handleDbStats st _ =+  withMetrics st (.db) $ do+    let db = st.config.store.db.db+    memory <- (memoryReport db)+    statsM <- (getProperty db Stats)+    textResponse . cs $ memory <> maybe "Could not get stats" cs statsM++-----------------------+-- Parameter Parsing --+-----------------------++-- | Returns @Nothing@ if the parameter is not supplied. Raises an exception on+-- parse failure.+paramOptional :: forall a. (Param a) => WebState -> Req -> IO (Maybe a)+paramOptional st req = do+  let net = st.config.store.net+  let ctx = st.config.store.ctx+  param req label >>= \case+    Nothing -> return Nothing -- Parameter was not supplied+    Just t -> Just <$> readParam (parseParam net ctx . T.splitOn ",") label t+  where+    label = proxyLabel (Proxy :: Proxy a)++-- | Raises an exception if the parameter is not supplied+paramRequired :: forall a. (Param a) => WebState -> Req -> IO a+paramRequired st req = maybe (raise st err) return =<< paramOptional st req+  where+    label = T.unpack (proxyLabel (Proxy :: Proxy a))+    err = UserError $ "Missing parameter " <> label++-- | Returns the default value of a parameter if it is not supplied. Raises an+-- exception on parse failure.+paramDef :: (Default a, Param a) => WebState -> Req -> IO a+paramDef st req = fromMaybe def <$> paramOptional st req++-- | Does not raise exceptions. Passes the request to the next route ('next') if the parameter is+-- not supplied or if parsing fails.+paramCapture :: forall a. (Param a) => WebState -> Req -> IO a+paramCapture st req = do+  let net = st.config.store.net+  let ctx = st.config.store.ctx+  p <- capture req label+  maybe next return (parseParam net ctx (T.splitOn "," p))+  where+    label = proxyLabel (Proxy :: Proxy a)++-- | Read the transaction that a request carries in its body.+parseTxBody :: WebState -> Req -> IO Tx+parseTxBody st req = do+  let net = st.config.store.net+  case decodeTxBody net (L.toStrict req.body) of+    Left _ -> raise st $ UserError "Failed to parse request body"+    Right tx -> return tx++-- | Read a transaction from a request body. The body is the transaction in+-- hex or as raw bytes. White space in the hex is ignored.+--+-- The body must hold one transaction and nothing else. Bytes after the+-- transaction are refused, as Bitcoin Core does for a raw transaction.+--+-- The transaction must be in the format that peers of the network read. A+-- network without SegWit, such as Bitcoin Cash, has the format without+-- witness only. Its nodes would not read the SegWit format, so the store+-- does not take it either.+decodeTxBody :: Network -> C.ByteString -> Either String Tx+decodeTxBody net body = fromHex <> fromBytes body+  where+    fromBytes = runGetS $ do+      tx <- getNetworkTx net+      done <- isEmpty+      unless done $ fail "Trailing bytes after transaction"+      return tx+    fromHex =+      case parseBase16 (C.filter (not . isSpace) body) of+        Right b16 -> fromBytes (decodeBase16 b16)+        Left e -> Left (cs e)++-- | Offset in the @offset@ parameter. Responds with an error if above the maximum.+parseOffset :: WebState -> Req -> IO OffsetParam+parseOffset st req = do+  res@(OffsetParam o) <- paramDef st req+  let limits = st.config.limits+  when (limits.maxOffset > 0 && fromIntegral o > limits.maxOffset) $+    raise st . UserError $+      "offset exceeded: " <> show o <> " > " <> show limits.maxOffset+  return res++-- | The best block, if there is one.+bestBlock :: StoreRead -> IO (Maybe BlockData)+bestBlock q = q.getBestBlock >>= maybe (return Nothing) q.getBlock++-- | Turn a start parameter into a position: a transaction, the block of a block+-- hash, a height, or the last block at or before a time.+parseStart ::+  WebState ->+  Maybe StartParam ->+  IO (Maybe Start)+parseStart _ Nothing = return Nothing+parseStart st (Just s) = case s of+  -- a transaction if there is one with that hash, else a block+  StartParamHash {hash = h} ->+    st.query.getTxData (TxHash h) >>= \case+      Just _ -> return (Just (AtTx (TxHash h)))+      Nothing -> fmap (AtBlock . (.height)) <$> st.query.getBlock (H.BlockHash h)+  StartParamHeight {height = h} -> return (Just (AtBlock (fromIntegral h)))+  StartParamTime {time = q} ->+    fmap (AtBlock . (.height)) <$> C.blockAtOrBefore st.query st.config.store.chain q++-- | Limit, offset and start in the parameters of a request.+parseLimits :: WebState -> Req -> IO LimitsParam+parseLimits st req = LimitsParam <$> paramOptional st req <*> parseOffset st req <*> paramOptional st req++-- | Pagination of a query from request parameters, within the limits for full or short items.+paramToLimits ::+  WebState ->+  Bool ->+  LimitsParam ->+  IO Limits+paramToLimits st full (LimitsParam limitM o startM) = do+  let wl = st.config.limits+  Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart st startM++-- | Limit of a query: the requested one or the default, but no more than the maximum.+validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32+validateLimit wl full limitM =+  f m $ maybe d (fromIntegral . (.get)) limitM+  where+    m+      | full && wl.maxFullItemCount > 0 = wl.maxFullItemCount+      | otherwise = wl.maxItemCount+    d = wl.defItemCount+    f a 0 = a+    f 0 b = b+    f a b = min a b++---------------+-- Utilities --+---------------++-- | The queries that the web server answers with: those of the database,+-- with the Redis cache, if there is one, in front of the queries about xpubs+-- and of the bodies of transactions. An xpub that the cache does not have is+-- read from the database, and cached for next time if it is large enough.+--+-- When Redis does not answer, every query is answered from the database, as+-- if the store had no cache. The variable says whether Redis answered the+-- last time: a query that fails clears it, so that the next ones do not wait+-- for Redis in turn, and the health check sets it again when Redis is back.+webQuery :: Store -> TVar Bool -> StoreRead -> StoreRead+webQuery s up base = case s.cache of+  Nothing -> base+  Just c ->+    (base :: StoreRead)+      { getTxData = fmap listToMaybe' . txData c . pure,+        getManyTxData = txData c,+        xPubBals = \x ->+          cached (cacheXPubBals c x) >>= \case+            Just bals -> return bals+            Nothing -> do+              bals <- base.xPubBals x+              _ <- cached (Just <$> cacheRequest c x bals)+              return bals,+        xPubUnspents = \x bals l ->+          cached (cacheXPubUnspents c x bals l) >>= maybe (base.xPubUnspents x bals l) return,+        xPubTxs = \x bals l ->+          cached (cacheXPubTxs c x l) >>= maybe (base.xPubTxs x bals l) return,+        xPubTxCount = \x bals ->+          cached (cacheXPubTxCount c x) >>= maybe (base.xPubTxCount x bals) return+      }+  where+    -- ask Redis, unless it is down, and take a failure, or no answer within+    -- two seconds, for "not cached"+    cached :: IO (Maybe a) -> IO (Maybe a)+    cached ask =+      readTVarIO up >>= \case+        False -> return Nothing+        True ->+          (timeout 2000000 ask `catchAny` \_ -> return Nothing) >>= \case+            Just answer -> return answer+            Nothing -> atomically (writeTVar up False) >> return Nothing+    -- bodies from Redis when the cache has them, so that the transactions+    -- of a cached xpub are served in full even before this store has+    -- imported them, and from the database otherwise+    txData _ [] = return []+    txData c ths = do+      found <- cached (Just <$> cacheTxData c ths)+      let fromCache = fromMaybe (map (const Nothing) ths) found+          missing = [h | (h, Nothing) <- zip ths fromCache]+      fromDB <- HashMap.fromList . zip missing <$> base.getManyTxData missing+      -- deleted ones are left out, wherever they come from+      return $+        zipWith+          (\h m -> notDeleted (maybe (fromMaybe Nothing (HashMap.lookup h fromDB)) Just m))+          ths+          fromCache++-- | The state of the web server, answering without the cache if the flag is+-- 'True', as @nocache=true@ asks.+cacheOr :: Bool -> WebState -> WebState+cacheOr False st = st+cacheOr True st = (st :: WebState) {query = st.direct}++-- | The only element of a list of one optional value.+listToMaybe' :: [Maybe a] -> Maybe a+listToMaybe' [x] = x+listToMaybe' _ = Nothing++-- | Middleware that logs every request with its response status: errors at error level, others at debug level.+logIt :: Logger -> Middleware+logIt logger app req respond =+  app req $ \res -> do+    let s = responseStatus res+        msg = fmtReq req <> ": " <> fmtStatus s+    if statusIsSuccessful s+      then logDebug logger msg+      else logError logger msg+    respond res++-- | Describe a request for logs.+fmtReq :: Request -> Text+fmtReq req =+  let m = requestMethod req+      v = httpVersion req+      p = rawPathInfo req+      q = rawQueryString req+   in T.decodeUtf8 (m <> " " <> p <> q <> " " <> cs (show v))++-- | Describe a response status for logs.+fmtStatus :: Status -> Text+fmtStatus s = cs (show (statusCode s)) <> " " <> cs (statusMessage s)++-- | A parameter read with a parser, or the default if there is none. Answers+-- with an error if it does not parse ('readParam').+paramOr :: Req -> (Text -> Maybe a) -> Text -> a -> IO a+paramOr req f name d = param req name >>= maybe (return d) (readParam f name)++-- | A parameter read with a parser. Answers with an error if there is none or+-- it does not parse ('readParam').+requireParam :: WebState -> Req -> (Text -> Maybe a) -> Text -> IO a+requireParam st req f name =+  param req name >>= maybe (raise st (UserError ("Missing parameter " <> T.unpack name))) (readParam f name)++-- | The value of a parameter, read with a parser. Answers with an error that+-- names the parameter and the value if it does not parse: a value that the+-- server does not understand is a mistake of the client, and answering as if+-- it had not been given would hide the mistake.+readParam :: (Text -> Maybe a) -> Text -> Text -> IO a+readParam f name t =+  maybe (throwIO (invalidParam name t)) return (f t)++-- | The error for a value of a parameter that does not parse.+invalidParam :: Text -> Text -> Except+invalidParam name t =+  UserError . T.unpack $ "Invalid value for parameter " <> name <> ": " <> t++-- | A count, offset or number of confirmations: a whole number, not negative.+readNat :: Text -> Maybe Natural+readNat t = readMaybe (T.unpack t) >>= \(n :: Integer) -> if n < 0 then Nothing else Just (fromInteger n)++-- | @true@ or @false@, in any case.+readBool :: Text -> Maybe Bool+readBool t = case T.toLower t of+  "true" -> Just True+  "false" -> Just False+  _ -> Nothing++-- | A number in decimal.+readNum :: (Read a) => Text -> Maybe a+readNum = readMaybe . T.unpack
+ src/Haskoin/Store/Web/Docs.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module      : Haskoin.Store.Web.Docs+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The reference of the API, served by the server that it describes: the+-- page at the root of the server, and the description that the page reads.+--+-- = Why the server carries its own reference+--+-- @swagger.yaml@ is the reference of the API. A copy on a web site+-- describes whatever version that site was last given, and someone who+-- runs a server wants the reference of the server in front of them. So the+-- file is compiled into the program ('swaggerYaml'), and the root of the+-- server answers with a page that shows it ('swaggerPage'), with the+-- server itself first in its list of servers, so that requests tried from+-- the page go to it.+--+-- = What the page needs+--+-- The page is Swagger UI, loaded from a public CDN at a fixed version, as+-- the page of api.haskoin.com does. A browser without access to the+-- Internet shows an empty page; @\/swagger.yaml@ is served either way. The+-- program is not made a megabyte and a half larger to carry a copy of it.+module Haskoin.Store.Web.Docs+  ( swaggerYaml,+    swaggerPage,+  )+where++import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.FileEmbed (embedFile, makeRelativeToProject)++-- | @swagger.yaml@ as it was when the program was built, with this server+-- at the head of its list of servers: a relative address, which a reader of+-- the description resolves against where it found the description.+swaggerYaml :: ByteString+swaggerYaml = case B.breakSubstring marker file of+  (before, rest)+    | B.null rest -> file+    | otherwise -> before <> marker <> "  - url: './'\n    description: This server\n" <> B.drop (B.length marker) rest+  where+    marker = "servers:\n"+    file = $(makeRelativeToProject "swagger.yaml" >>= embedFile)++-- | The page at the root of the server: Swagger UI, showing 'swaggerYaml',+-- which it asks this server for, next to wherever the page itself is.+swaggerPage :: ByteString+swaggerPage =+  B.intercalate+    "\n"+    [ "<!DOCTYPE html>",+      "<html lang=\"en\">",+      "<head>",+      "  <meta charset=\"UTF-8\">",+      "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",+      "  <title>Haskoin Store API</title>",+      "  <link rel=\"stylesheet\" href=\"" <> cdn <> "/swagger-ui.css\">",+      "</head>",+      "<body>",+      "  <div id=\"swagger-ui\"></div>",+      "  <noscript>The reference of this API is at <a href=\"swagger.yaml\">swagger.yaml</a>.</noscript>",+      "  <script src=\"" <> cdn <> "/swagger-ui-standalone-preset.js\"></script>",+      "  <script src=\"" <> cdn <> "/swagger-ui-bundle.js\" charset=\"UTF-8\"></script>",+      "  <script>",+      "    window.onload = function () {",+      "      // next to this page, whether or not its address ends in a slash",+      "      const here = window.location.href.split(/[?#]/)[0].replace(/\\/?$/, '/');",+      "      window.ui = SwaggerUIBundle({",+      "        url: new URL('swagger.yaml', here).href,",+      "        dom_id: '#swagger-ui',",+      "        presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],",+      "        layout: 'StandaloneLayout'",+      "      });",+      "    };",+      "  </script>",+      "</body>",+      "</html>",+      ""+    ]+  where+    cdn = "https://unpkg.com/swagger-ui-dist@5.17.14"
+ src/Haskoin/Store/Web/Events.hs view
@@ -0,0 +1,380 @@+{-# LANGUAGE MultiWayIf #-}++-- |+-- Module      : Haskoin.Store.Web.Events+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- What the clients of @\/events@ are told: a log of the last events that+-- every client reads at its own pace, and the choice that a client makes of+-- the transactions it wants to hear about.+--+-- = One log instead of a queue for each client+--+-- Every client used to have a queue of its own, filled as events came. A+-- queue knows nothing of what came before it was made, so a client that+-- lost its connection for a second had no way to ask for what it missed,+-- and had to read everything again.+--+-- Here the events are kept once, numbered, in an t'EventLog' of the last+-- 'logLength'. A client is a number, the next event that it has not read+-- ('readEvents'). A client that connects starts at the end+-- ('eventsEnd'), or after an event that it names by its hash+-- ('eventsAfter'), which is how it takes up where it left. A client that+-- is slower than the events come falls off the start of the log and is+-- told so, as it was when its queue filled.+--+-- An event is named by its hash and not by its number, because the number+-- is that of one server since it started. In a fleet every server reads+-- the same stream of events from Redis, from the start of what Redis+-- keeps, so the logs of the servers hold the same events in the same+-- order, and a client may come back to another server than the one it+-- left.+--+-- A transaction can be in the log twice, when it entered the mempool and+-- when it left. A client that names it is taken to have heard the first,+-- and hears everything after it again: an event told twice costs a+-- request, and an event not told is a payment that a wallet does not show.+--+-- = Choosing transactions+--+-- A client of the WebSocket can say which transactions it cares for, with+-- the commands of t'EventCommand': addresses, extended public keys, and a+-- Bloom filter as in BIP 37. What it chose is its t'Watch', and 'relevant'+-- decides for each transaction. So that it can, the log keeps the+-- transaction and its addresses with the event (see t'Seen'), read once when+-- the event comes, whatever the number of clients.+--+-- When the server does not know the transaction of an event, every client+-- is told of it. That happens when a transaction leaves the mempool long+-- after the log forgot that it entered, and it errs on the side of telling+-- too much.+--+-- An extended public key is watched as a wallet uses it: the addresses in+-- use and the gap after them on each of its two chains. When a transaction+-- touches one, the gap moves on ('relevant' returns the new t'Watch'), so+-- the client goes on hearing of payments to addresses it had not been+-- given yet when it subscribed. The same key may be watched under each+-- type of address, with a command for each.+--+-- The Bloom filter is tested as a node tests it for an SPV client+-- ('bloomRelevantUpdate'), except that the server does not add the+-- outputs of a matching transaction to the filter: the filter stays as the+-- client made it, and the client adds what it wants to follow+-- ('EventFilterAdd'). A filter that changed behind the back of the client+-- could not be told apart from its own after a few matches.+module Haskoin.Store.Web.Events+  ( -- * Log+    EventLog,+    Happened (..),+    Entry (..),+    Seen (..),+    seenOf,+    newEventLog,+    appendEvent,+    eventsEnd,+    eventsAfter,+    readEvents,+    logLength,+    logWeight,++    -- * Choice of a client+    Watch (..),+    everything,+    nothing,+    relevant,+    command,+    maxWatchAddresses,+    maxWatchXPubs,+  )+where++import Control.Concurrent.STM+  ( STM,+    TVar,+    atomically,+    modifyTVar',+    newTVarIO,+    readTVar,+    retry,+    stateTVar,+  )+import Data.ByteString qualified as B+import Data.Either (rights)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.IntMap.Strict qualified as IntMap+import Data.Maybe (isJust, mapMaybe)+import Data.Sequence (Seq, ViewL (..), (|>))+import Data.Sequence qualified as Seq+import Data.Word (Word32, Word64)+import Haskoin+  ( Address,+    BlockHash,+    BloomFilter,+    Ctx,+    Network,+    Tx (..),+    TxHash,+    TxOut (..),+    bloomInsert,+    bloomRelevantUpdate,+    pubSubKey,+    scriptToAddressBS,+  )+import Haskoin.Store.Common (deriveFunction)+import Haskoin.Store.Data+  ( Event (..),+    EventCommand (..),+    Prev (..),+    TxData (..),+    XPubBal (..),+    XPubSpec (..),+    nullBalance,+  )++-- | What the server knows of a transaction that an event is about: enough+-- to say whether a client wants to hear of it.+data Seen = Seen+  { -- | the transaction, for the Bloom filter+    tx :: !Tx,+    -- | the addresses that it pays to and spends from+    addresses :: !(HashSet Address),+    -- | roughly the bytes that this takes in memory+    size :: !Int+  }+  deriving (Eq, Show)++-- | A transaction as the log keeps it.+seenOf :: Network -> Ctx -> TxData -> Seen+seenOf net ctx td =+  Seen+    { tx = td.tx,+      addresses = HashSet.fromList (rights (map (scriptToAddressBS net ctx) scripts)),+      size = 256 + sum (map B.length scripts) + sum (map (sum . map B.length) td.tx.witness)+    }+  where+    scripts = map (.script) (IntMap.elems td.prevs) <> map (.script) td.tx.outputs++-- | Something that the clients are told.+data Happened+  = -- | the best block changed to this one+    BlockArrived !BlockHash+  | -- | a transaction entered the mempool, with what is known of it+    TxEntered !TxHash !(Maybe Seen)+  | -- | a transaction left the mempool unconfirmed+    TxLeft !TxHash+  deriving (Eq, Show)++-- | An event in the log.+data Entry = Entry+  { -- | its place: one more than that of the event before+    number :: !Word64,+    -- | what the client is sent+    event :: !Event,+    -- | the transaction, when the event is about one that the server knows+    seen :: !(Maybe Seen),+    -- | the bytes that this entry is counted for in the log+    weight :: !Int+  }+  deriving (Eq, Show)++-- | The last events, oldest first.+data Log = Log+  { entries :: !(Seq Entry),+    -- | the number of the next event+    next :: !Word64,+    -- | the sum of the weights of the entries+    held :: !Int,+    -- | the transactions of the entries that say that one entered, to+    -- know them again when they leave+    bodies :: !(HashMap TxHash Seen)+  }++-- | The log of events that the clients of a server read.+newtype EventLog = EventLog (TVar Log)++-- | The most events that the log keeps: how far back a client can take up+-- from, and how far behind it may fall.+logLength :: Int+logLength = 10000++-- | The most bytes of transactions that the log keeps. Events of large+-- transactions make the log shorter instead of making the server larger.+logWeight :: Int+logWeight = 64 * 1024 * 1024++-- | An empty log.+newEventLog :: IO EventLog+newEventLog = EventLog <$> newTVarIO Log {entries = Seq.empty, next = 0, held = 0, bodies = HashMap.empty}++-- | Add an event at the end of the log, and drop from the start what the+-- log has no room for.+appendEvent :: EventLog -> Happened -> STM ()+appendEvent (EventLog var) what = modifyTVar' var $ \l ->+  let entry = case what of+        BlockArrived h -> Entry {number = l.next, event = EventBlock h, seen = Nothing, weight = 0}+        TxEntered h s -> Entry {number = l.next, event = EventTx h, seen = s, weight = maybe 0 (.size) s}+        TxLeft h -> Entry {number = l.next, event = EventTx h, seen = HashMap.lookup h l.bodies, weight = 0}+      bodies = case what of+        TxEntered h (Just s) -> HashMap.insert h s l.bodies+        _ -> l.bodies+   in trim Log {entries = l.entries |> entry, next = l.next + 1, held = l.held + entry.weight, bodies}+  where+    trim l = case Seq.viewl l.entries of+      e :< rest+        | Seq.length l.entries > logLength || l.held > logWeight ->+            trim Log {entries = rest, next = l.next, held = l.held - e.weight, bodies = forget e l.bodies}+      _ -> l+    forget e = case e.event of+      EventTx h | e.weight > 0 -> HashMap.delete h+      _ -> id++-- | Where a client that wants only what comes from now on starts reading.+eventsEnd :: EventLog -> STM Word64+eventsEnd (EventLog var) = (.next) <$> readTVar var++-- | Where a client that has heard of an event starts reading, given the+-- hash of the block or of the transaction: after the first event about+-- it. 'Nothing' when the log has no such event, because it never had or+-- because it is too long ago; the client then has to read what it follows+-- again and start from the end.+eventsAfter :: EventLog -> Maybe BlockHash -> Maybe TxHash -> STM (Maybe Word64)+eventsAfter (EventLog var) block tx = do+  l <- readTVar var+  let is e = case e.event of+        EventBlock h -> Just h == block+        EventTx h -> Just h == tx+  return $ (\i -> (Seq.index l.entries i).number + 1) <$> Seq.findIndexL is l.entries++-- | The events from a number on, waiting for one when there is none yet.+-- 'Nothing' when the log no longer has that number: the client fell too+-- far behind.+readEvents :: EventLog -> Word64 -> STM (Maybe (Seq Entry))+readEvents (EventLog var) from = do+  l <- readTVar var+  let first = l.next - fromIntegral (Seq.length l.entries)+  if+    | from < first -> return Nothing+    | from >= l.next -> retry+    | otherwise -> return (Just (Seq.drop (fromIntegral (from - first)) l.entries))++-- | What a client of the WebSocket chose to hear about.+data Watch = Watch+  { -- | whether it chose at all; a client that did not hears everything+    filtered :: !Bool,+    -- | the addresses that it named+    plain :: !(HashSet Address),+    -- | the keys that it named, with the number of addresses derived on+    -- the external and on the change chain+    keys :: !(HashMap XPubSpec (Word32, Word32)),+    -- | the addresses derived from the keys: the key, the chain and the+    -- index of each+    derived :: !(HashMap Address (XPubSpec, Word32, Word32)),+    -- | its Bloom filter+    bloom :: !(Maybe BloomFilter)+  }+  deriving (Eq, Show)++-- | The choice of a client that has not chosen: every transaction.+everything :: Watch+everything =+  Watch {filtered = False, plain = HashSet.empty, keys = HashMap.empty, derived = HashMap.empty, bloom = Nothing}++-- | The choice of a client that wants blocks alone, until it names+-- something to watch.+nothing :: Watch+nothing = everything {filtered = True}++-- | The most addresses that a client may name. Those derived from keys do+-- not count.+maxWatchAddresses :: Int+maxWatchAddresses = 100000++-- | The most extended public keys that a client may name. Each is read+-- from the database when it is named, as a request about it is.+maxWatchXPubs :: Int+maxWatchXPubs = 100++-- | Whether a client is told of an event, given what the log knows of its+-- transaction, and its choice afterwards: the gap of a key moves on when+-- the transaction touches one of its addresses. The gap is the number of+-- unused addresses to watch after the last one used.+--+-- A client that has not chosen is told of everything, and every client is+-- told of a block and of a transaction that the server does not know.+relevant :: Ctx -> Word32 -> Maybe Seen -> Watch -> (Bool, Watch)+relevant _ _ Nothing w = (True, w)+relevant ctx gap (Just s) w+  | not w.filtered = (True, w)+  | otherwise = (named || not (null hits) || matched, foldl' moved w hits)+  where+    touched = HashSet.toList s.addresses+    named = any (`HashSet.member` w.plain) touched+    hits = mapMaybe (`HashMap.lookup` w.derived) touched+    matched = maybe False (\f -> isJust (bloomRelevantUpdate ctx f s.tx)) w.bloom+    moved now (spec, chain, index) = extend ctx spec chain (index + 1 + gap) now++-- | Derive the addresses of a watched key on one chain up to a number of+-- them, if there are fewer.+extend :: Ctx -> XPubSpec -> Word32 -> Word32 -> Watch -> Watch+extend ctx spec chain upto w = case HashMap.lookup spec w.keys of+  Just (external, change)+    | upto > have ->+        w+          { keys = HashMap.insert spec (if chain == 0 then (upto, change) else (external, upto)) w.keys,+            derived = foldl' (\m i -> HashMap.insert (derive i) (spec, chain, i) m) w.derived [have .. upto - 1]+          }+    where+      have = if chain == 0 then external else change+      derive = deriveFunction ctx spec.deriv (pubSubKey ctx spec.key chain)+  _ -> w++-- | Do what a client asked with a command, and say why not when it cannot+-- be done, in which case the connection is to be closed. The function+-- reads the balances of an extended public key from the database.+command :: Ctx -> Word32 -> (XPubSpec -> IO [XPubBal]) -> TVar Watch -> EventCommand -> IO (Either String ())+command ctx gap balances var = \case+  EventWatch as -> change $ \w ->+    let plain = foldl' (flip HashSet.insert) w.plain as+     in if HashSet.size plain > maxWatchAddresses+          then Left "Too many addresses"+          else Right (chosen w) {plain}+  EventUnwatch as -> change $ \w -> Right (chosen w) {plain = foldl' (flip HashSet.delete) w.plain as}+  EventWatchXPub spec -> do+    -- read before the choice changes: nothing is held while the database+    -- is read+    used <- balances spec+    let upto chain = gap + maximum (0 : [i + 1 | XPubBal {path = [c, i], balance = b} <- used, c == chain, inUse b])+    change $ \w ->+      if+        | HashMap.member spec w.keys -> Right (chosen w)+        | HashMap.size w.keys >= maxWatchXPubs -> Left "Too many extended public keys"+        | otherwise ->+            let named = (chosen w) {keys = HashMap.insert spec (0, 0) w.keys}+             in Right (extend ctx spec 1 (upto 1) (extend ctx spec 0 (upto 0) named))+  EventUnwatchXPub spec -> change $ \w ->+    Right+      (chosen w)+        { keys = HashMap.delete spec w.keys,+          derived = HashMap.filter (\(s, _, _) -> s /= spec) w.derived+        }+  EventFilterLoad f -> change $ \w -> Right (chosen w) {bloom = Just f}+  EventFilterAdd d -> change $ \w -> case w.bloom of+    Nothing -> Left "No filter to add to"+    Just f -> Right w {bloom = Just (bloomInsert f d)}+  EventFilterClear -> change $ \w -> Right (chosen w) {bloom = Nothing}+  EventAll -> change (const (Right everything))+  EventNone -> change (const (Right nothing))+  where+    chosen w = w {filtered = True}+    inUse b = not (nullBalance b)+    change :: (Watch -> Either String Watch) -> IO (Either String ())+    change f = atomically . stateTVar var $ \w -> case f w of+      Left why -> (Left why, w)+      Right new -> (Right (), new)
+ src/Haskoin/Store/Web/Router.hs view
@@ -0,0 +1,231 @@+-- |+-- Module      : Haskoin.Store.Web.Router+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Routing of requests to handlers, directly over WAI.+--+-- A 'Route' is a method, a path pattern such as @\/block\/:block@, the names+-- of the parameters it accepts in the query string or a form, and a handler:+-- a function from the request to a response in 'IO'. 'router' turns a list of+-- them into a WAI 'Application' that answers with the first route whose+-- method and path match. A handler that finds the request is not for it after+-- all, such as one whose path parameter does not parse as the kind of value+-- it serves, calls 'next', and the request goes to the next route that+-- matches.+--+-- = Parameters are declared+--+-- A request with a parameter that its route does not accept is refused+-- ('Unknown'), rather than answered as if the parameter were not there. A+-- misspelt parameter, such as @limt=10@, or one that an endpoint does not+-- support, would otherwise be ignored, and the client would get an answer to+-- a question it did not ask, with nothing to tell it so. The check runs+-- before the handler, so a refused request has no effect, which matters for+-- a handler that does something, such as broadcasting a transaction.+--+-- The declaration and the handler cannot drift apart: reading a parameter+-- that the route does not declare ('param') throws 'Undeclared', an error of+-- the program that the tests of each endpoint find, rather than a parameter+-- that is accepted and never read, or read and never accepted.+--+-- = Why not a web framework+--+-- The server needs routing, parameters and responses; a framework like+-- Scotty adds them in a monad transformer over 'IO' that every handler has to+-- run in, with its own exceptions for control flow and its own way to return+-- a response. Here a handler is a plain function: it gets everything about+-- the request in a 'Req', returns a WAI 'Response', and stops early by+-- throwing, which 'router' and the server catch. The whole router is this+-- module.+module Haskoin.Store.Web.Router+  ( -- * Routes+    Route (..),+    Miss (..),+    Undeclared (..),+    route,+    get,+    post,+    router,+    routerScoped,++    -- * Requests+    Req (..),+    Next (..),+    next,+    capture,+    captureWith,+    param,+    header,+  )+where++import Control.Applicative ((<|>))+import Control.Exception (Exception, catch, throwIO)+import Data.ByteString.Lazy qualified as L+import Data.CaseInsensitive qualified as CI+import Data.List (nub)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding (decodeUtf8Lenient, encodeUtf8)+import Network.HTTP.Types (Method, methodGet, methodPost, parseQueryText)+import Network.Wai++-- | A request being answered.+data Req = Req+  { -- | the request of WAI+    wai :: !Request,+    -- | the parameters captured from the path, by name+    captures :: ![(Text, Text)],+    -- | the parameters of the query string, then those of a form in the+    -- body; the first one of a name is the one that counts+    params :: ![(Text, Text)],+    -- | the body, read whole before the handler runs+    body :: !L.ByteString,+    -- | the names of the parameters that the route accepts+    accepted :: ![Text]+  }++-- | Where a request should go.+data Route = Route+  { method :: !Method,+    -- | the segments of the path: literals, and captures by name+    pattern :: ![Either Text Text],+    -- | the names of the parameters it accepts in the query string or a form+    accepts :: ![Text],+    handler :: Req -> IO Response+  }++-- | A route for a method, a path such as @\/block\/:block@, where a segment+-- that starts with a colon captures what is there under its name, and the+-- names of the parameters it accepts.+route :: Method -> Text -> [Text] -> (Req -> IO Response) -> Route+route m path = Route m [segment t | t <- T.splitOn "/" path, not (T.null t)]+  where+    segment t = maybe (Left t) Right (T.stripPrefix ":" t)++-- | A route for @GET@.+get :: Text -> [Text] -> (Req -> IO Response) -> Route+get = route methodGet++-- | A route for @POST@.+post :: Text -> [Text] -> (Req -> IO Response) -> Route+post = route methodPost++-- | Why no route answered a request.+data Miss+  = -- | no route has its method and path+    NoRoute+  | -- | the routes with its method and path do not accept these parameters+    Unknown ![Text]+  deriving stock (Show)++-- | What 'param' throws for a parameter that the route does not declare: a+-- mistake in the program, not in the request.+newtype Undeclared = Undeclared Text+  deriving stock (Show)++instance Exception Undeclared++-- | What a handler throws to pass the request to the next route that+-- matches.+data Next = Next+  deriving stock (Show)++instance Exception Next++-- | Pass the request to the next route that matches.+next :: IO a+next = throwIO Next++-- | An application that answers with the first route whose method and path+-- match the request, that accepts all its parameters, and whose handler does+-- not pass it on; or with the fallback, told why none did. The body of the+-- request is read whole first, and parsed as a form if it is one; handlers+-- that stream a response still stream it.+router :: [Route] -> (Req -> Miss -> IO Response) -> Application+router routes = routerScoped routes ($ routes)++-- | 'router' for handlers that need something that lasts as long as one+-- request does, such as a view of a database that must be the same for+-- everything the request reads and must be let go when it is answered.+--+-- The routes are given twice. The first list is used for its methods, paths+-- and parameters only, to find the route of a request. The second argument+-- runs an action with the same routes, in the same order, made for the one+-- request: the handler that answers is taken from those. The response is+-- sent inside that action, so that a handler that streams its response can+-- go on reading while it is sent.+--+-- The alternative, to make the routes once and have each handler take what+-- it needs for itself, would put that in every handler; and making all the+-- routes again for each request, to match against, would cost more than+-- many requests do.+routerScoped ::+  [Route] ->+  (([Route] -> IO (Maybe ResponseReceived)) -> IO (Maybe ResponseReceived)) ->+  (Req -> Miss -> IO Response) ->+  Application+routerScoped routes scoped fallback request respond = do+  body <- strictRequestBody request+  let form+        | lookup "Content-Type" (requestHeaders request)+            == Just "application/x-www-form-urlencoded" =+            parseQueryText (L.toStrict body)+        | otherwise = []+      params = [(k, maybe "" id v) | (k, v) <- queryToText (queryString request) <> form]+      req cs as = Req {wai = request, captures = cs, params = params, body = body, accepted = as}+      path = filter (not . T.null) (pathInfo request)+      -- the parameters that the first route matching the path refused, if any+      go unknown [] = fallback (req [] []) (maybe NoRoute Unknown unknown) >>= respond+      go unknown ((i, r) : rs)+        | r.method == requestMethod request,+          Just cs <- match r.pattern path =+            case nub [k | (k, _) <- params, k `notElem` r.accepts] of+              [] -> scoped (answer i (req cs r.accepts)) >>= maybe (go unknown rs) return+              ks -> go (unknown <|> Just ks) rs+        | otherwise = go unknown rs+  go Nothing (zip [0 :: Int ..] routes)+  where+    -- the response of the handler of a route, sent, or nothing if the+    -- handler passed the request on+    answer i r scopedRoutes =+      ((Just <$> (scopedRoutes !! i).handler r) `catch` \Next -> return Nothing) >>= mapM respond+    queryToText q = [(decodeUtf8Lenient k, decodeUtf8Lenient <$> v) | (k, v) <- q]++-- | The captures of a path, if it matches a pattern.+match :: [Either Text Text] -> [Text] -> Maybe [(Text, Text)]+match [] [] = Just []+match (Left l : ps) (t : ts) | l == t = match ps ts+match (Right n : ps) (t : ts) = ((n, t) :) <$> match ps ts+match _ _ = Nothing++-- | A parameter captured from the path. Passes the request on if there is+-- none of that name, which only a wrong pattern would cause.+capture :: Req -> Text -> IO Text+capture r n = maybe next return (lookup n r.captures)++-- | A parameter captured from the path, read with a parser. Passes the+-- request on if it does not parse: the path may be meant for another route,+-- such as an xpub where an address is expected.+captureWith :: Req -> (Text -> Maybe a) -> Text -> IO a+captureWith r f n = capture r n >>= maybe next return . f++-- | A parameter of the query string or of a form, if there is one. Throws+-- 'Undeclared' if the route does not accept a parameter of that name.+--+-- It is an action rather than a pure function so that the error is thrown+-- when the parameter is read, inside the handler, where the server answers+-- it, and not whenever the value happens to be evaluated, which may be while+-- the response is being sent.+param :: Req -> Text -> IO (Maybe Text)+param r n+  | n `elem` r.accepted = return (lookup n r.params)+  | otherwise = throwIO (Undeclared n)++-- | A header of the request, if there is one.+header :: Req -> Text -> Maybe Text+header r n = decodeUtf8Lenient <$> lookup (CI.mk (encodeUtf8 n)) (requestHeaders r.wai)
+ stress/Main.hs view
@@ -0,0 +1,913 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module      : Main+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Stress test of the Redis cache of xpubs. Run it with+-- @scripts/stress-cache.sh@, which builds it, runs it and writes a report.+--+-- It starts a regtest chain of Bitcoin Core, Valkey and wrk in Podman+-- containers from the test image ("Haskoin.Store.Integration.Image"), and a+-- haskoin-store server with the cache on. A few wallets, whose keys this+-- program holds, get many coins; then the program makes transactions between+-- them, signed here and sent to the node in batches, and mines a block after+-- each batch. Every transaction changes two cached xpubs, so the cache+-- reloads them again and again while wrk asks the server about them on five+-- endpoints at once. At the end, the cached answers are compared with those+-- of the database.+--+-- Everything it measures goes to files in the output directory, as JSON+-- lines: @rounds.jsonl@ (one line per block), @samples.jsonl@ (one line per+-- second: heights, CPU, memory and the StatsD metrics of the server),+-- @wrk.jsonl@ (one line per run of wrk) and @summary.json@.+-- @scripts/stress-report.py@ turns them into a report.+--+-- = Why it works this way+--+-- * __Transactions are signed here.__ A node wallet makes a few hundred+--   transactions per second at best, and slower as it grows; signing with+--   the keys in memory and sending raw transactions in batches keeps the node+--   busy only with validating them.+-- * __Only confirmed coins are spent.__ A block is mined after each batch, so+--   chains of unconfirmed transactions, which the node limits, never form.+-- * __The server is a separate process__, the release binary, so that its+--   CPU and memory are its own, and it is configured from the command line+--   as in production.+-- * __Containers are removed__ when the program ends, even when it fails:+--   they run with @--rm@ inside brackets, and carry a label with the process+--   ID, so that @scripts/stress-cache.sh@ and the next run remove any that a+--   killed run left.+module Main (main) where++import Control.Concurrent.STM (TVar, atomically, newTVarIO, readTVarIO, writeTVar)+import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Control.Lens ((&), (.~), (?~), (^.))+import Control.Monad (forM, forM_, unless, void, when)+import Data.Aeson (Value (..), eitherDecode, encode, object, (.=))+import Data.Aeson qualified as A+import Data.ByteString qualified as BS+import Data.ByteString.Char8 qualified as C8+import Data.ByteString.Lazy qualified as BL+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize)+import Data.FileEmbed (embedFile, makeRelativeToProject)+import Data.Foldable (toList)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.IORef (IORef, atomicModifyIORef', newIORef)+import Data.List (sort)+import Data.Maybe (fromMaybe, mapMaybe)+import Data.Sequence (Seq)+import Data.Sequence qualified as Seq+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding (decodeUtf8)+import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime)+import Data.Word (Word64)+import Database.Redis qualified as Redis+import Haskoin hiding (Coin, Key)+import Haskoin.Store.Integration.Image (testImage, valkeyBin, wrkBin)+import Haskoin.Store.Integration.Node+import Haskoin.Store.Integration.Podman+import Haskoin.Store.Integration.Valkey (redisContainer, waitRedis)+import Network.Socket qualified as Socket+import Network.Socket.ByteString qualified as SocketB+import Network.Wreq qualified as Wreq+import Numeric (readHex)+import Options.Applicative+import System.Exit (ExitCode (..))+import System.IO (Handle, IOMode (..), hFlush, hPutStrLn, stderr, withFile)+import System.Process+import System.Random.Stateful (IOGenM, StdGen, mkStdGen, newIOGenM, uniformRM)+import Text.Read (readMaybe)+import UnliftIO (MVar, bracket, newMVar, tryAny, withAsync, withMVar)+import UnliftIO.Concurrent (threadDelay)+import UnliftIO.Directory (createDirectoryIfMissing, makeAbsolute)++-------------------------------------------------------------------------------+-- Options+-------------------------------------------------------------------------------++data Opts = Opts+  { txs :: !Int,+    wallets :: !Int,+    addresses :: !Int,+    blockTxs :: !Int,+    batch :: !Int,+    connections :: !Int,+    wrkSeconds :: !Int,+    server :: !FilePath,+    serverRts :: !String,+    out :: !FilePath,+    chainMb :: !Int,+    seed :: !Int+  }++opts :: Parser Opts+opts = do+  txs <- num "txs" 200000 "Transactions to make between the wallets"+  wallets <- num "wallets" 4 "Wallets, each an xpub"+  addresses <- num "addresses" 200 "Receiving and change addresses of each wallet"+  blockTxs <- num "block-txs" 2000 "Transactions per block"+  batch <- num "batch" 250 "Transactions per call to the node"+  connections <- num "connections" 2 "Connections of wrk to each endpoint"+  wrkSeconds <- num "wrk-seconds" 10 "Seconds of each run of wrk"+  server <- strOption (long "server" <> metavar "PATH" <> help "haskoin-store binary")+  serverRts <-+    strOption $+      long "server-rts"+        <> metavar "FLAGS"+        <> value "-N4 -M2g"+        <> showDefault+        <> help "RTS flags of the server"+  out <- strOption (long "out" <> metavar "DIR" <> help "Directory for the results")+  chainMb <- num "chain-mb" 1024 "Size of the tmpfs of the chain of the node, in MiB"+  seed <- num "seed" 1 "Seed of the random choices"+  return Opts {..}+  where+    num name def h = option auto (long name <> metavar "N" <> value def <> showDefault <> help h)++-------------------------------------------------------------------------------+-- Main+-------------------------------------------------------------------------------++main :: IO ()+main = do+  o <- execParser (info (opts <**> helper) (fullDesc <> progDesc "Stress test of the Redis cache of haskoin-store"))+  outDir <- makeAbsolute o.out+  createDirectoryIfMissing True outDir+  podmanUsable >>= either fail return+  say "Building or finding the test image"+  image <- testImage+  withContext $ \ctx ->+    withContainer (valkey image) $ \rc -> do+      redisURL <- ("redis://127.0.0.1:" <>) . show <$> hostPort rc 6379+      waitRedis redisURL+      withContainer (nodeSpec o image) $ \nc -> do+        rpcPort <- hostPort nc 18443+        p2p <- hostPort nc 18444+        let node = Node {kind = BitcoinCore, url = "http://127.0.0.1:" <> show rpcPort <> "/", p2p = p2p}+        waitRpc node+        withContainer (wrkSpec image outDir) $ \wc ->+          withStatsd $ \statsPort stats ->+            withRamDir $ \dir ->+              withServer o outDir dir redisURL p2p statsPort $ \srv ->+                stress o {out = outDir} ctx node redisURL srv stats wc++-- | Valkey with room for the bodies of every transaction.+valkey :: String -> ContainerSpec+valkey image =+  let spec = redisContainer image+   in spec+        { options = ["--memory", "1g", "--entrypoint", valkeyBin],+          command = map (\a -> if a == "128mb" then "768mb" else a) spec.command+        }++-- | The regtest node, with a larger chain and mempool than in the tests.+nodeSpec :: Opts -> String -> ContainerSpec+nodeSpec o image =+  let spec = nodeContainer BitcoinCore image+   in spec+        { options = map grow spec.options,+          command = map bigger spec.command <> ["-rpcworkqueue=64", "-rpcthreads=8"]+        }+  where+    grow "1g" = "3g"+    grow a+      | "/chain:" `T.isPrefixOf` T.pack a = "/chain:rw,size=" <> show o.chainMb <> "m,mode=1777"+      | otherwise = a+    bigger "-dbcache=16" = "-dbcache=128"+    bigger "-maxmempool=16" = "-maxmempool=128"+    bigger a = a++-- | A container that runs wrk against the server, on the network of the+-- host, with the output directory, where the scripts of wrk are, mounted.+wrkSpec :: String -> FilePath -> ContainerSpec+wrkSpec image dir =+  ContainerSpec+    { name = "wrk",+      image = image,+      options =+        [ "--network",+          "host",+          "--memory",+          "512m",+          "--security-opt",+          "label=disable",+          "--volume",+          dir <> ":/stress:ro",+          "--entrypoint",+          "sleep"+        ],+      command = ["infinity"],+      ports = []+    }++-------------------------------------------------------------------------------+-- The server+-------------------------------------------------------------------------------++data Server = Server+  { url :: !String,+    pid :: !Int+  }++-- | Run the haskoin-store server while an action runs.+withServer :: Opts -> FilePath -> FilePath -> String -> Int -> Int -> (Server -> IO a) -> IO a+withServer o outDir dir redisURL p2p statsPort act =+  withFile (outDir <> "/server.log") WriteMode $ \logH -> do+    -- another program could take the port before the server binds it, but+    -- the server then fails to start, which is reported+    port <- freePort+    let gap = o.addresses + 20+        args =+          [ "--dir",+            dir,+            "--host",+            "127.0.0.1",+            "--port",+            show port,+            "--net",+            "btcreg",+            "--peer",+            "127.0.0.1:" <> show p2p,+            "--max-peers",+            "1",+            "--redis",+            "--redis-url",+            redisURL,+            "--redis-min-addrs",+            "10",+            "--xpub-gap",+            show gap,+            "--xpub-gap-init",+            show gap,+            "--statsd",+            "--statsd-host",+            "127.0.0.1",+            "--statsd-port",+            show statsPort,+            "--statsd-prefix",+            "stress",+            "--statsd-aggregates",+            "--ticker-url",+            "http://127.0.0.1:9/",+            "--price-history-url",+            "http://127.0.0.1:9/",+            "+RTS"+          ]+            <> words o.serverRts+            <> ["-RTS"]+        cp = (proc o.server args) {std_out = UseHandle logH, std_err = UseHandle logH}+    bracket (createProcess cp) stop $ \(_, _, _, ph) -> do+      pid <- maybe (fail "The server did not start") (return . fromIntegral) =<< getPid ph+      writeFile (outDir <> "/server.pid") (show pid)+      let srv = Server {url = "http://127.0.0.1:" <> show port, pid = pid}+      waitFor 60 "the server to answer" $ do+        getProcessExitCode ph >>= \case+          Just c -> fail ("The server exited with " <> show c <> "; see server.log")+          Nothing -> either (const False) (const True) <$> tryAny (getJSON srv "/health")+      act srv+  where+    stop (_, _, _, ph) = terminateProcess ph >> void (waitForProcess ph)++-- | A free TCP port of the loopback interface.+freePort :: IO Int+freePort =+  bracket (Socket.socket Socket.AF_INET Socket.Stream Socket.defaultProtocol) Socket.close $ \s -> do+    Socket.bind s (Socket.SockAddrInet 0 (Socket.tupleToHostAddress (127, 0, 0, 1)))+    fromIntegral <$> Socket.socketPort s++-- | JSON answer of the server.+getJSON :: Server -> String -> IO Value+getJSON srv path = do+  let o = Wreq.defaults & Wreq.checkResponse ?~ (\_ _ -> return ())+  r <- Wreq.getWith o (srv.url <> path)+  either fail return (eitherDecode (r ^. Wreq.responseBody))++-------------------------------------------------------------------------------+-- Wallets+-------------------------------------------------------------------------------++-- | An address and its key.+data Key = Key+  { script :: !ScriptOutput,+    secret :: !SecKey+  }++-- | A wallet: its xpub, and its receiving and change addresses.+data Wallet = Wallet+  { xpub :: !Text,+    receive :: !(Seq Key),+    change :: !(Seq Key)+  }++-- | A coin that a wallet can spend.+data Coin = Coin+  { outpoint :: !OutPoint,+    value :: !Word64,+    wallet :: !Int,+    key :: !Key+  }++-- | The wallets, derived from a fixed seed, and the key of the miner.+makeWallets :: Ctx -> Opts -> ([Wallet], Key)+makeWallets ctx o = ([wallet (fromIntegral i) | i <- [0 .. o.wallets - 1]], keyOf (hardSubKey ctx root 9999) 0 0)+  where+    root = makeXPrvKey (C8.pack ("haskoin-store-stress-" <> show o.seed))+    wallet i =+      let w = hardSubKey ctx root i+       in Wallet+            { xpub = xPubExport btcRegTest ctx (deriveXPubKey ctx w),+              receive = Seq.fromList [keyOf w 0 j | j <- [0 .. fromIntegral o.addresses - 1]],+              change = Seq.fromList [keyOf w 1 j | j <- [0 .. fromIntegral o.addresses - 1]]+            }+    keyOf w c j =+      let k = prvSubKey ctx (prvSubKey ctx w c) j+          a = xPubAddr ctx (deriveXPubKey ctx k)+       in Key {script = fromMaybe (error "no script") (addressToOutput a), secret = k.key}++-- | Address of a key, as text.+keyAddress :: Key -> Text+keyAddress k = case k.script of+  PayPKHash h -> fromMaybe (error "no address") (addrToText btcRegTest (PubKeyAddress h))+  _ -> error "not a key hash"++-- | Sign a transaction that spends coins.+sign :: Ctx -> Tx -> [Coin] -> Tx+sign ctx tx coins =+  either error id $+    signTx+      btcRegTest+      ctx+      tx+      [SigInput c.key.script c.value c.outpoint sigHashAll Nothing | c <- coins]+      [c.key.secret | c <- coins]++-- | A payment from a coin to another wallet, with change back to the wallet+-- of the coin. Gives the transaction and its new coins.+pay :: Ctx -> IOGenM StdGen -> [Wallet] -> Coin -> IO (Tx, [Coin])+pay ctx g ws c = do+  w' <- (\i -> (c.wallet + i) `mod` length ws) <$> uniformRM (1, max 1 (length ws - 1)) g+  let to = ws !! w'+      from = ws !! c.wallet+  r <- (`Seq.index` 0) . flip Seq.drop to.receive <$> uniformRM (0, Seq.length to.receive - 1) g+  ch <- (`Seq.index` 0) . flip Seq.drop from.change <$> uniformRM (0, Seq.length from.change - 1) g+  amount <- uniformRM (10000, max 10000 (c.value `div` 2)) g+  -- every output is worth at least 10000, so a coin always pays the fee+  let fee = 1000+      outs+        | c.value >= amount + fee + 10000 = [(r.script, amount), (ch.script, c.value - amount - fee)]+        | otherwise = [(r.script, c.value - fee)]+      tx = sign ctx (buildTx ctx [c.outpoint] outs) [c]+      h = txHash tx+      owners = [(w', r), (c.wallet, ch)]+      coins = [Coin (OutPoint h i) v w k | (i, (_, v), (w, k)) <- zip3 [0 ..] outs owners]+  return (tx, coins)++-------------------------------------------------------------------------------+-- The node+-------------------------------------------------------------------------------++-- | Send transactions to the node in one call. Gives the error of each+-- transaction that the node refused.+sendRaw :: Node -> [Tx] -> IO [Maybe Value]+sendRaw node txs = do+  let o =+        Wreq.defaults+          & Wreq.auth ?~ Wreq.basicAuth "test" "test"+          & Wreq.checkResponse ?~ (\_ _ -> return ())+          & Wreq.header "Content-Type" .~ ["application/json"]+      body =+        A.toJSON+          [ object+              [ "jsonrpc" .= ("1.0" :: Text),+                "id" .= i,+                "method" .= ("sendrawtransaction" :: Text),+                "params" .= [encodeHex (runPutS (serialize tx))]+              ]+          | (i, tx) <- zip [0 :: Int ..] txs+          ]+  r <- Wreq.postWith o node.url body+  case eitherDecode (r ^. Wreq.responseBody) of+    Right (Array xs) ->+      return+        [ case field "error" x of+            Just Null -> Nothing+            Nothing -> Nothing+            e -> e+        | x <- toList xs+        ]+    Right v -> fail ("Unexpected answer to a batch: " <> take 400 (show v))+    Left e -> fail ("Bad answer to a batch: " <> e)++-- | The coinbase output of a block.+coinbaseOf :: Node -> Text -> IO (OutPoint, Word64)+coinbaseOf node h = do+  b <- rpc node "getblock" [String h, Number 2]+  case field "tx" b of+    Just (Array ts) | Just t <- Seq.lookup 0 (Seq.fromList (toList ts)) -> do+      txid <- maybe (fail "no txid") return (textField "txid" t >>= hexToTxHash)+      v <- case field "vout" t of+        Just (Array os) | o : _ <- toList os, Just v <- field "value" o -> return (sats v)+        _ -> fail "no coinbase output"+      return (OutPoint txid 0, v)+    _ -> fail ("No transactions in block " <> T.unpack h)++-- | Height of the best block of the node.+nodeHeight :: Node -> IO Int+nodeHeight node =+  rpc node "getblockcount" [] >>= \case+    Number n -> return (round n)+    v -> fail ("Unexpected block count: " <> show v)++-------------------------------------------------------------------------------+-- The stress test+-------------------------------------------------------------------------------++stress :: Opts -> Ctx -> Node -> String -> Server -> IORef Stats -> Container -> IO ()+stress o ctx node redisURL srv stats wc = do+  g <- newIOGenM (mkStdGen o.seed)+  let (ws, miner) = makeWallets ctx o+      minerAddr = keyAddress miner+      perFanOut = 1000+      fanOuts = max 1 ((o.blockTxs * 3 `div` 2 + perFanOut - 1) `div` perFanOut)+  writeJSON (o.out <> "/config.json") $+    object+      [ "txs" .= o.txs,+        "wallets" .= o.wallets,+        "addresses" .= o.addresses,+        "block_txs" .= o.blockTxs,+        "batch" .= o.batch,+        "connections" .= o.connections,+        "wrk_seconds" .= o.wrkSeconds,+        "server_rts" .= o.serverRts,+        "xpubs" .= map (.xpub) ws+      ]+  say ("Funding " <> show (length ws) <> " wallets with " <> show (fanOuts * perFanOut) <> " coins")+  funded <- generateTo node fanOuts minerAddr+  void $ generateTo node 100 minerAddr+  coins0 <- fmap concat . forM funded $ \h -> do+    (op, v) <- coinbaseOf node h+    let n = perFanOut+        each = (v - 200000) `div` fromIntegral n+        -- every receiving address of every wallet, in turn+        targets = take n (cycle [(wi, Seq.index w.receive j) | j <- [0 .. o.addresses - 1], (wi, w) <- zip [0 ..] ws])+        tx = sign ctx (buildTx ctx [op] [(k.script, each) | (_, k) <- targets]) [Coin op v (-1) miner]+    errs <- sendRaw node [tx]+    forM_ errs $ mapM_ (\e -> fail ("The node refused a funding transaction: " <> show e))+    return [Coin (OutPoint (txHash tx) i) each wi k | (i, (wi, k)) <- zip [0 ..] targets]+  void $ generateTo node 1 minerAddr+  say "Waiting for the server to sync and cache the xpubs"+  waitSynced node srv+  forM_ ws $ \w -> getJSON srv ("/xpub/" <> T.unpack w.xpub <> "/balances")+  waitFor 120 "the xpubs to be cached" $ do+    conn <- Redis.checkedConnect (either error id (Redis.parseConnectInfo redisURL))+    n <- either (const 0) id <$> Redis.runRedis conn (Redis.dbsize)+    Redis.disconnect conn+    return (n > 0)+  writeScripts o+  slowlogStart redisURL+  stop <- newTVarIO False+  start <- getCurrentTime+  withFile (o.out <> "/samples.jsonl") WriteMode $ \sh ->+    withFile (o.out <> "/wrk.jsonl") WriteMode $ \wh -> do+      wlock <- newMVar wh+      withAsync (sampler start node srv redisURL stats stop sh) $ \_ ->+        withAsync (wrkAll o ws srv wc start stop wlock) $ \_ -> do+          (sent, refused) <- withFile (o.out <> "/rounds.jsonl") WriteMode $ \rh ->+            rounds o ctx g node ws minerAddr start rh (Seq.fromList coins0)+          say "Waiting for the server to catch up"+          t0 <- getCurrentTime+          waitSynced node srv+          catchUp <- (`diffUTCTime` t0) <$> getCurrentTime+          atomically (writeTVar stop True)+          -- let the last runs of wrk and the sampler finish+          threadDelay ((o.wrkSeconds + 3) * 1000000)+          say "Comparing cached answers with the database"+          checks <- forM ws (consistent srv)+          slow <- slowlog redisURL+          end <- getCurrentTime+          height <- nodeHeight node+          writeJSON (o.out <> "/summary.json") $+            object+              [ "transactions_sent" .= sent,+                "transactions_refused" .= refused,+                "height" .= height,+                "seconds" .= (realToFrac (end `diffUTCTime` start) :: Double),+                "catch_up_seconds" .= (realToFrac catchUp :: Double),+                "consistency" .= concat checks,+                "redis_slow" .= slow+              ]+          say ("Done: " <> show sent <> " transactions, " <> show refused <> " refused")++-- | Make the transactions, a block at a time. Gives how many were sent and+-- how many the node refused.+rounds :: Opts -> Ctx -> IOGenM StdGen -> Node -> [Wallet] -> Text -> UTCTime -> Handle -> Seq Coin -> IO (Int, Int)+rounds o ctx g node ws minerAddr start rh = go 0 (0, 0)+  where+    total = o.txs+    go :: Int -> (Int, Int) -> Seq Coin -> IO (Int, Int)+    go r (sent, refused) pool+      | sent >= total = return (sent, refused)+      | otherwise = do+          let n = minimum [o.blockTxs, total - sent, Seq.length pool]+              (spend, rest) = Seq.splitAt n pool+          when (n == 0) $ fail "No coins left to spend"+          t0 <- getCurrentTime+          made <- mapM (pay ctx g ws) (toList spend)+          -- sign now, not when the transactions are sent+          mapM_ (evaluate . force . fst) made+          t1 <- getCurrentTime+          results <- fmap concat . forM (chunks o.batch (zip (toList spend) made)) $ \chunk -> do+            errs <- sendRaw node [tx | (_, (tx, _)) <- chunk]+            return (zip chunk errs)+          t2 <- getCurrentTime+          let accepted = [new | ((_, (_, new)), Nothing) <- results]+              back = [c | ((c, _), Just _) <- results]+              errors = [e | (_, Just e) <- results]+          case errors of+            e : _ ->+              say $+                unwords+                  [ "The node refused",+                    show (length errors),+                    "transactions, such as",+                    take 300 (show e)+                  ]+            [] -> return ()+          void $ generateTo node 1 minerAddr+          t3 <- getCurrentTime+          height <- nodeHeight node+          let sent' = sent + length accepted+          appendLine rh $+            object+              [ "round" .= r,+                "t" .= secs start t3,+                "height" .= height,+                "sent" .= sent',+                "refused" .= length errors,+                "sign_ms" .= ms t0 t1,+                "submit_ms" .= ms t1 t2,+                "mine_ms" .= ms t2 t3+              ]+          when (r `mod` 10 == 0) $+            say ("Round " <> show r <> ": " <> show sent' <> " transactions, height " <> show height)+          go (r + 1) (sent', refused + length errors) (rest <> Seq.fromList (back <> concat accepted))+    chunks k xs = case splitAt k xs of+      ([], _) -> []+      (a, b) -> a : chunks k b++-- | Wait until the server has the best block of the node.+waitSynced :: Node -> Server -> IO ()+waitSynced node srv =+  waitFor 1800 "the server to sync" $ do+    h <- nodeHeight node+    serverHeight srv <&&> (== h)+  where+    a <&&> f = maybe False f <$> a++-- | Height of the best block of the server.+serverHeight :: Server -> IO (Maybe Int)+serverHeight srv =+  tryAny (getJSON srv "/block/best?notx=true") <&> \case+    Right v | Just (Number n) <- field "height" v -> Just (round n)+    _ -> Nothing+  where+    (<&>) = flip fmap++-- | Compare the cached answers about an xpub with those of the database,+-- for up to a minute, as the cache may still be catching up.+consistent :: Server -> Wallet -> IO [Value]+consistent srv w =+  forM paths $ \p -> do+    ok <- tryWait (60 :: Int) p+    return (object ["xpub" .= w.xpub, "path" .= p, "same" .= ok])+  where+    paths = ["", "/balances", "/transactions?limit=100", "/unspent?limit=100"]+    base = "/xpub/" <> T.unpack w.xpub+    sep p = if '?' `elem` p then "&" else "?"+    tryWait n p = do+      a <- getJSON srv (base <> p)+      b <- getJSON srv (base <> p <> sep p <> "nocache=true")+      if a == b || n <= 0+        then return (a == b)+        else threadDelay 1000000 >> tryWait (n - 1) p++-------------------------------------------------------------------------------+-- wrk+-------------------------------------------------------------------------------++-- | Endpoints that wrk asks about, each rotating over every xpub.+endpoints :: [(String, Text -> Text)]+endpoints =+  [ ("summary", \x -> "/xpub/" <> x),+    ("balances", \x -> "/xpub/" <> x <> "/balances"),+    ("transactions", \x -> "/xpub/" <> x <> "/transactions?limit=100"),+    ("unspent", \x -> "/xpub/" <> x <> "/unspent?limit=100"),+    ("multiaddr", \x -> "/blockchain/multiaddr?active=" <> x <> "&n=50")+  ]++-- | The script of wrk ("stress/wrk.lua"), copied into the output directory+-- where the container of wrk sees it. It asks for the paths it is given in+-- turn and prints its results as JSON. It is embedded when the program is+-- compiled, so the program finds it wherever it runs.+writeScripts :: Opts -> IO ()+writeScripts o = BS.writeFile (o.out <> "/wrk.lua") $(makeRelativeToProject "stress/wrk.lua" >>= embedFile)++-- | Run wrk on every endpoint at once, over and over, until told to stop.+wrkAll :: Opts -> [Wallet] -> Server -> Container -> UTCTime -> TVar Bool -> MVar Handle -> IO ()+wrkAll o ws srv wc start stop lock = go endpoints+  where+    go [] = return ()+    go ((name, path) : rest) = withAsync (loop name (map (path . (.xpub)) ws)) $ \_ -> go rest >> waitStop+    waitStop = readTVarIO stop >>= \s -> unless s (threadDelay 500000 >> waitStop)+    loop name paths = do+      s <- readTVarIO stop+      unless s $ do+        t <- getCurrentTime+        let args =+              [ "exec",+                wc.cid,+                wrkBin,+                "-t1",+                "-c" <> show o.connections,+                "-d" <> show o.wrkSeconds <> "s",+                "--timeout",+                "10s",+                "-s",+                "/stress/wrk.lua",+                srv.url,+                "--"+              ]+                <> map T.unpack paths+        (code, out, err) <- readProcessWithExitCode "podman" args ""+        let result = case [l | l <- lines out, take 1 l == "{"] of+              l : _ | Just (Object r) <- A.decode (BL.fromStrict (C8.pack l)) -> Object r+              _ -> object ["failed" .= True, "exit" .= show code, "stderr" .= take 300 err]+        withMVar lock $ \h ->+          appendLine h (merge (object ["t" .= secs start t, "endpoint" .= name]) result)+        loop name paths+    merge (Object a) (Object b) = Object (a <> b)+    merge a _ = a++-------------------------------------------------------------------------------+-- Sampling+-------------------------------------------------------------------------------++-- | Write a sample every second until told to stop: the heights of the node+-- and the server, the CPU and memory of the server, the memory of Valkey, and+-- the StatsD metrics of the server since the last sample.+sampler :: UTCTime -> Node -> Server -> String -> IORef Stats -> TVar Bool -> Handle -> IO ()+sampler start node srv redisURL stats stop h = do+  hz <- clockTicks+  conn <- Redis.checkedConnect (either error id (Redis.parseConnectInfo redisURL))+  let go prevTicks prevTime = do+        s <- readTVarIO stop+        unless s $ do+          threadDelay 1000000+          now <- getCurrentTime+          ticks <- cpuTicks srv.pid+          rss <- rssKb srv.pid+          (heap, lazyFree) <- ghcHeapKb srv.pid+          nh <- either (const Nothing) Just <$> tryAny (nodeHeight node)+          sh <- serverHeight srv+          vm <- valkeyMemory conn+          m <- drainStats stats+          let seconds = realToFrac (now `diffUTCTime` prevTime)+              cpu = fromIntegral (max 0 (ticks - prevTicks)) / fromIntegral hz / seconds * 100 :: Double+          appendLine h $+            object+              [ "t" .= secs start now,+                "node_height" .= nh,+                "server_height" .= sh,+                "cpu_percent" .= cpu,+                "rss_mb" .= (fromIntegral rss / 1024 :: Double),+                "heap_mb" .= (fromIntegral heap / 1024 :: Double),+                "heap_lazy_free_mb" .= (fromIntegral lazyFree / 1024 :: Double),+                "valkey_mb" .= ((/ (1024 * 1024)) . fromIntegral <$> vm :: Maybe Double),+                "counters" .= m.counters,+                "gauges" .= m.gauges,+                "timings" .= HashMap.map timingStats m.timings+              ]+          go ticks now+  t <- getCurrentTime+  ticks0 <- cpuTicks srv.pid+  go ticks0 t+  where+    timingStats vs =+      let s = sort vs+          n = length s+          at p = s !! min (n - 1) (floor (p * fromIntegral n :: Double))+       in object ["n" .= n, "p50" .= at 0.5, "p95" .= at 0.95, "max" .= last s, "sum" .= sum s]++-- | Ticks of the clock of the kernel per second.+clockTicks :: IO Int+clockTicks = do+  r <- tryAny (readProcessWithExitCode "getconf" ["CLK_TCK"] "")+  return $ case r of+    Right (ExitSuccess, o, _) | Just n <- readMaybe o -> n+    _ -> 100++-- | CPU time of a process, in ticks of the clock of the kernel.+cpuTicks :: Int -> IO Int+cpuTicks pid = do+  r <- tryAny (readFile ("/proc/" <> show pid <> "/stat"))+  return $ case r of+    -- the fields after the name, which is in parentheses and may have spaces+    Right s+      | fs <- words (drop 2 (dropWhile (/= ')') s)),+        length fs > 13 ->+          sum (mapMaybe readMaybe [fs !! 11, fs !! 12])+    _ -> 0++-- | Resident memory of a process, in KiB.+rssKb :: Int -> IO Int+rssKb pid = do+  r <- tryAny (readFile ("/proc/" <> show pid <> "/status"))+  return $ case r of+    Right s | (v : _) <- [w | l <- lines s, ("VmRSS:" : w : _) <- [words l]], Just n <- readMaybe v -> n+    _ -> 0++-- | Resident memory of the GHC heap of a process, in KiB: what is resident of+-- the address range that the runtime reserves for its heap, 1 TiB from+-- @0x4200000000@. The rest of the resident memory is outside the heap:+-- RocksDB, buffers of C libraries, stacks and the program itself. A heap that+-- grows while its live data does not is memory the runtime took for a peak+-- and kept, which a heap profile, made of live data, does not show.+--+-- Also gives how much of it the runtime has given back to the system with+-- @MADV_FREE@ (@LazyFree@): free for the kernel to take when it needs memory,+-- but counted as resident until it does.+ghcHeapKb :: Int -> IO (Int, Int)+ghcHeapKb pid = do+  r <- tryAny (readFile ("/proc/" <> show pid <> "/smaps"))+  return $ case r of+    Left _ -> (0, 0)+    Right s -> (go "Rss:" False 0 (lines s), go "LazyFree:" False 0 (lines s))+  where+    base = 0x4200000000 :: Integer+    top = base + 2 ^ (40 :: Int)+    go _ _ n [] = n+    go key inHeap n (l : ls)+      | (range : _) <- words l,+        (a, '-' : _) <- break (== '-') range,+        [(start, "")] <- readHex a =+          go key (start >= base && start < top) n ls+      | inHeap, (k : v : _) <- words l, k == key, Just x <- readMaybe v = go key inHeap (n + x) ls+      | otherwise = go key inHeap n ls++-- | Memory used by Valkey, in bytes.+valkeyMemory :: Redis.Connection -> IO (Maybe Integer)+valkeyMemory conn =+  Redis.runRedis conn Redis.info <&&> \case+    Right s ->+      case [v | l <- C8.lines s, Just v <- [C8.stripPrefix "used_memory:" l]] of+        v : _ -> fst <$> C8.readInteger v+        [] -> Nothing+    Left _ -> Nothing+  where+    a <&&> f = f <$> a++-- | Make Valkey log every command that takes 10 ms or more, keeping all of+-- them: a long script blocks every client of Redis, every server included.+slowlogStart :: String -> IO ()+slowlogStart url = withRedis url $ \conn -> Redis.runRedis conn $ do+  send ["CONFIG", "SET", "slowlog-log-slower-than", "10000"]+  send ["CONFIG", "SET", "slowlog-max-len", "100000"]+  send ["SLOWLOG", "RESET"]+  where+    send args = void (Redis.sendRequest args :: Redis.Redis (Either Redis.Reply Redis.Reply))++-- | The commands that took 10 ms or more: when, how long in milliseconds,+-- the command, and how many arguments it had. For a script of the cache,+-- also its name and, for a commit, whether it was of a full reload.+--+-- The slow log keeps the first 32 arguments of a command, each cut to 128+-- bytes, and ends the list with how many it left out. That is enough: the+-- first argument of an EVAL is the script, whose first line names it (as+-- @commitScript@), and the number of arguments of a commit grows with the+-- size of the change.+slowlog :: String -> IO [Value]+slowlog url = withRedis url $ \conn ->+  Redis.runRedis conn (Redis.sendRequest ["SLOWLOG", "GET", "100000"]) <&&> \case+    Right (Redis.MultiBulk (Just entries)) -> mapMaybe entry entries+    _ -> []+  where+    a <&&> f = f <$> a+    entry (Redis.MultiBulk (Just (_ : Redis.Integer ts : Redis.Integer us : Redis.MultiBulk (Just reply) : _))) =+      let args = [decodeUtf8 b | Redis.Bulk (Just b) <- reply]+          script = case args of+            _ : body : _ -> take 1 [w | w <- T.words body, "Script" `T.isSuffixOf` w]+            _ -> []+       in Just . object $+            [ "time" .= ts,+              "ms" .= (fromIntegral us / 1000 :: Double),+              "command" .= T.toUpper (T.take 24 (mconcat (take 1 args))),+              "args" .= argCount args+            ]+              <> ["script" .= s | s <- script]+              -- ARGV[8] of a commit is the time of a full reload, or empty+              <> ["full" .= not (T.null f) | script == ["commitScript"], f <- take 1 (drop 10 args)]+    entry _ = Nothing+    -- the arguments kept, the last of which may say how many were left out:+    -- "... (2048 more arguments)"+    argCount args = case reverse args of+      l : kept+        | Just rest <- T.stripPrefix "... (" l,+          [(n, _)] <- reads (T.unpack rest) ->+            length kept + n :: Int+      _ -> length args++withRedis :: String -> (Redis.Connection -> IO a) -> IO a+withRedis url = bracket (Redis.checkedConnect (either error id (Redis.parseConnectInfo url))) Redis.disconnect++-------------------------------------------------------------------------------+-- StatsD+-------------------------------------------------------------------------------++-- | Metrics received since they were last drained: counters added up,+-- gauges at their last value, and every timing.+data Stats = Stats+  { counters :: !(HashMap Text Double),+    gauges :: !(HashMap Text Double),+    timings :: !(HashMap Text [Double])+  }++-- | Receive the StatsD metrics of the server on a UDP port of the loopback+-- interface while an action runs.+withStatsd :: (Int -> IORef Stats -> IO a) -> IO a+withStatsd act =+  bracket (Socket.socket Socket.AF_INET Socket.Datagram Socket.defaultProtocol) Socket.close $ \s -> do+    Socket.bind s (Socket.SockAddrInet 0 (Socket.tupleToHostAddress (127, 0, 0, 1)))+    port <- fromIntegral <$> Socket.socketPort s+    ref <- newIORef (Stats HashMap.empty HashMap.empty HashMap.empty)+    let receive = do+          bs <- SocketB.recv s 65535+          forM_ (C8.lines bs) $ \l -> forM_ (parseLine l) $ \m ->+            atomicModifyIORef' ref (\st -> (add m st, ()))+          receive+    withAsync receive $ \_ -> act port ref+  where+    add (k, v, t) st = case t of+      'c' -> st {counters = HashMap.insertWith (+) k v st.counters}+      'g' -> st {gauges = HashMap.insert k v st.gauges}+      _ -> st {timings = HashMap.insertWith (<>) k [v] st.timings}++-- | A StatsD line, @key:value|type@ with an optional @|\@rate@: the key+-- without the prefix of the server, the value (scaled up by the rate for a+-- sampled counter), and the first letter of the type.+parseLine :: C8.ByteString -> Maybe (Text, Double, Char)+parseLine l = do+  let (k, rest) = C8.break (== ':') l+  (v, t, rate) <- case C8.split '|' (C8.drop 1 rest) of+    [v, t] -> Just (v, t, 1)+    [v, t, r] | Just ('@', x) <- C8.uncons r -> (v,t,) <$> readMaybe (C8.unpack x)+    _ -> Nothing+  x <- readMaybe (C8.unpack (C8.dropWhile (== '+') v))+  c <- fst <$> C8.uncons t+  let key = T.replace "stress.btcreg." "" (decodeUtf8 k)+  return (key, if c == 'c' then x / rate else x, c)++-- | Take the counters and timings received since the last call; gauges+-- keep their values.+drainStats :: IORef Stats -> IO Stats+drainStats ref = atomicModifyIORef' ref $ \st -> (st {counters = HashMap.empty, timings = HashMap.empty}, st)++-------------------------------------------------------------------------------+-- Helpers+-------------------------------------------------------------------------------++say :: String -> IO ()+say s = hPutStrLn stderr ("stress: " <> s) >> hFlush stderr++-- | Try a check every quarter of a second, for up to some seconds.+waitFor :: Int -> String -> IO Bool -> IO ()+waitFor secs' what check = go (secs' * 4)+  where+    go n =+      check >>= \case+        True -> return ()+        False+          | n <= 0 -> fail ("Timed out waiting for " <> what)+          | otherwise -> threadDelay 250000 >> go (n - 1)++secs :: UTCTime -> UTCTime -> Double+secs a b = realToFrac (b `diffUTCTime` a)++ms :: UTCTime -> UTCTime -> Double+ms a b = 1000 * secs a b++appendLine :: Handle -> Value -> IO ()+appendLine h v = BL.hPut h (encode v <> "\n") >> hFlush h++writeJSON :: FilePath -> Value -> IO ()+writeJSON path = BL.writeFile path . encode
+ stress/wrk.lua view
@@ -0,0 +1,32 @@+-- The script of wrk for the stress test (stress/Main.hs). It asks for the+-- paths given after "--" on the command line in turn, and when wrk is done it+-- prints one line of JSON with the results, latencies in microseconds:+--+--   wrk -s wrk.lua http://server -- /xpub/a /xpub/b+local paths = {}+local i = 0++init = function(args)+  paths = args+end++request = function()+  i = i % #paths + 1+  return wrk.format("GET", paths[i])+end++done = function(summary, latency, requests)+  local e = summary.errors+  local fields = {+    '"requests":' .. summary.requests,+    '"duration_us":' .. summary.duration,+    '"errors":' .. (e.connect + e.read + e.write + e.timeout),+    '"non2xx":' .. e.status,+    '"p50_us":' .. latency:percentile(50),+    '"p90_us":' .. latency:percentile(90),+    '"p99_us":' .. latency:percentile(99),+    '"max_us":' .. latency.max,+    '"mean_us":' .. string.format("%.1f", latency.mean),+  }+  io.write("{" .. table.concat(fields, ",") .. "}\n")+end
+ swagger.yaml view
@@ -0,0 +1,8042 @@+# Long values, such as the hexadecimal of transactions and blocks and the+# xpubs in the examples, are split into pieces of equal width with escaped line+# breaks in double quotes, which join the pieces with nothing between them, so+# that every line stays under 120 columns. An xpub used as a key is split the+# same way, as an explicit key ("? ").+openapi: 3.0.0++servers:+  - url: 'https://api.haskoin.com/bch/'+  - url: 'https://api.haskoin.com/btc/'+  - url: 'https://api.haskoin.com/bchtest4/'+  - url: 'https://api.haskoin.com/btctest4/'++info:+  version: &version 1.9.0+  title: Haskoin+  contact:+    name: JP Rupp+    url: https://github.com/jprupp+    email: jprupp@protonmail.ch+  license:+    name: MIT+    url: https://github.com/jprupp/haskoin-store/blob/master/haskoin-store/LICENSE+  description: |+    Haskoin Store indexes the Bitcoin (BTC) and Bitcoin Cash (BCH) block chains+    and answers the questions that a node cannot answer on its own: what is the+    balance of this address, which transactions belong to this wallet, which+    outputs can it spend. It connects to the peer-to-peer network itself, keeps+    its indices up to date with every block and every unconfirmed transaction,+    and serves them here.++    The servers of this reference are the public instance at api.haskoin.com, one per+    network: `btc`, `bch`, and their test networks `btctest4` and `bchtest4`. The+    same API is served by any Haskoin Store you run yourself.++    ## The basics++    - **Amounts** of coins are whole numbers of satoshis (1 coin is 100,000,000+      satoshis), never decimal numbers of coins: floating-point numbers cannot+      hold every satoshi amount exactly, and a balance that is off by one+      satoshi is a bug that is hard to find. Only prices in other currencies,+      in the Blockchain.info endpoints, are decimal.+    - **Hashes** of blocks and transactions are hexadecimal, in the byte order+      that nodes and block explorers display, so they can be copied between them+      and this API.+    - **Scripts and serialized data** are hexadecimal.+    - **Times** are Unix timestamps, in seconds.+    - **Addresses** are written as the network writes them: Base58 or Bech32 on+      BTC, CashAddr on BCH.++    ## Lists are newest first++    Every list of transactions or unspent outputs starts with the newest:+    unconfirmed transactions first, by the time this server first saw them, then+    confirmed ones, from the highest block down, and within a block from the last+    transaction to the first.++    This is the order in which wallets and explorers show activity, and the+    database is laid out in that order, so the first page of any list is a single+    lookup however long the history is.++    ## Paging++    Lists take three parameters:++    - `limit`: how many items to return. Without it the server returns its+      default number (100, unless the operator changed it). There is a maximum,+      lower for lists of full transactions, which are much larger; `limit=0`+      asks for that maximum.+    - `height`: where the page starts. It can be:+      - a **transaction id**: the page starts at that transaction, which is+        included;+      - a **block hash**: the page starts at that block;+      - a **block height**: the page starts at transactions confirmed at or below+        that height;+      - a **Unix time**: the page starts at the last block mined at or before+        that time.++      Numbers up to 1,230,768,000 are heights and larger ones are times. That+      number is midnight on 1 January 2009, before the first block, so no time+      of a block is mistaken for a height, and no chain will reach that height.+    - `offset`: how many items to skip after the start. The server has a+      maximum (50,000, unless the operator changed it).++    **To get the next page, pass the id of the last transaction you received as+    `height`, and `offset=1` to skip it.** An offset counts from the newest item,+    so every new transaction shifts every page by one, and a deep offset makes the+    server step over all the items before it. A transaction id is a fixed point:+    pages do not shift, and the server finds the start with one lookup however+    far back it is. The maximum offset is there because deep offsets are slow.++    ## Formats++    - **JSON** by default, without spaces, to keep responses small.+    - **Pretty JSON** with `pretty=true`, indented for people to read.+    - **Binary** with the header `Accept: application/octet-stream`: the compact+      serialization that the Haskell client in+      [haskoin-store-data](https://github.com/jprupp/haskoin-store-data)+      decodes. It is smaller and faster to decode than JSON. "Binary format"+      below gives every byte of it.++    Endpoints that end in `/raw` return data serialized as in the peer-to-peer+    protocol, in hexadecimal inside JSON (`{"result": "0100…"}`), or as bytes+    with the binary format.++    ## Parameters are checked++    Every endpoint lists the parameters it accepts, and refuses a request with+    any other, with 400 and a message that names it:++    ```json+    {"error": "user-error", "message": "Unknown parameter: limt"}+    ```++    A value that the endpoint cannot read is refused the same way, such as+    `limit=ten`, `notx=yes` or `derive=Legacy`. Without this, a misspelt+    parameter or a value the server does not understand would be ignored, and+    the client would get an answer to a question it did not ask, with nothing+    to tell it so. Requests are checked before anything is done, so a refused+    request has no effect.++    One thing is not refused, because it is not a mistake: a `limit` above the+    server's maximum is lowered to it, as a client cannot know the maximum of+    every server. Names of parameters are case-sensitive.++    ## Errors++    An error is a JSON object with a stable `error` code, for programs, and a+    `message`, for people:++    ```json+    {"error": "user-error", "message": "Invalid value for parameter limit: ten"}+    ```++    | Status | `error` | When |+    | --- | --- | --- |+    | 400 | `user-error`, `bad-request` | A parameter or the body is wrong; the message says which. |+    | 404 | `not-found-or-invalid-arg` | The thing does not exist, or a value in the path is not valid. |+    | 409 | `multiple-tx-index` | A short transaction number matches several transactions. |+    | 500 | `server-error` | Something went wrong on the server; it is logged there. |+    | 503 | | From `/health`: the server is not ready. |+    | 503 | `behind-fleet` | The server is one of a fleet and does not have the block that the fleet answers from; another server of the fleet does. |++    A request that asks for the binary format gets its errors in binary too:+    see "Binary format".++    `/health` also answers 203 for a server that works but cannot reach its+    Redis; see there.++    A value in the path that does not parse, such as a misspelt address, gives+    404 rather than 400. Paths are matched by the shape of what is in them, and+    a segment that does not parse may belong to another endpoint (`/block/best`+    and `/block/{block}` share a prefix), so the server tries the others, and+    when none matches it answers that there is nothing there. Query parameters+    are not ambiguous, so a bad one gives 400, with its name in the message.++    A 409 comes only from the Blockchain.info endpoints, which can name a+    transaction by a short number; the error lists the matching ids in `txids`.++    The codes `server-timeout` and `request-too-large` are part of the format, for+    clients that decode it, but this version of the server does not send them.++    ## Unconfirmed transactions++    The mempool is indexed like the chain. A payment shows up in balances and+    histories as soon as this server sees it on the network, with+    `"block": {"mempool": <time first seen>}` instead of a block height.++    A transaction can leave the mempool without being confirmed: when a block+    confirms another transaction that spends the same coins, or, on BTC, when a+    replacement that pays a higher fee arrives (replace-by-fee). It is then+    removed from every balance and history, together with any transaction that+    spends its outputs. From then on the server answers as if it had never+    seen it: fetching it by its id gives 404. Earlier versions answered with+    the transaction and `"deleted": true`, which clients that did not look at+    that field took for a pending payment.++    ## Reorganisations++    When the network switches to a chain with more work, the server undoes the+    blocks that are no longer in it, puts their transactions back in the+    mempool, and applies the new blocks. Every answer reflects the chain with+    the most work at the time of the request. Blocks of an abandoned chain keep+    answering by hash, with `"mainchain": false`.++    ## What the server trusts++    The server follows the chain with the most proof of work, as the network+    reports it, and checks what it needs to keep its indices consistent: that+    inputs exist and are not spent twice. It does not validate scripts or every+    consensus rule, as a full node does. Its answers are what the network says;+    for payments where a mistake would be costly, wait for confirmations, as with+    any wallet.++    ## Extended public keys++    Most wallets are an extended public key (xpub) from which they derive their+    addresses. The `/xpub` endpoints answer for all of them at once: the server+    derives the receiving chain (`0`) and the change chain (`1`), and looks at+    every address up to the last used one of each chain and then a gap of unused+    addresses after it (32, unless the operator changed it), and always at least+    the first 20 addresses. A payment to an address further away is not seen,+    as a wallet that follows BIP 44 with the same gap would not see it either.++    The `derive` parameter says which kind of address the wallet derives. An+    xpub does not say it, and the same key gives different addresses for each+    kind, so asking for the wrong kind would find nothing, and a wallet would+    show its user a zero balance. Instead, when a wallet has no transactions+    with the kind asked for but has some with another kind, the server answers+    400 with a message that names the right one. A wallet with no transactions+    of any kind is new, and gets an empty answer. The check only runs for+    wallets that look empty, so it costs nothing for wallets in use.++    Servers with a Redis cache keep large wallets ready: their first query reads+    the database and caches the result, and later ones are answered from memory.+    A cached answer is always the same as the database's; `nocache=true` skips+    the cache. The servers of a fleet share one such cache, and each answers+    from it what it would answer from its database at the block that the+    fleet answers from.++    ## Live updates++    Instead of polling, keep `/events` open: it sends a line for every new block+    and every change to the mempool, as a stream of JSON lines or over a+    WebSocket. Events carry only the hash; fetch what you need from it.++    The server keeps its last 10,000 events. A client that lost its connection+    comes back with `?after=` and the hash of the last event it heard, and gets+    what came after it. A client that falls further behind than that is+    disconnected, so that a slow client costs the server nothing.++    Over the WebSocket a client can choose the transactions it hears about: by+    address, by extended public key, or with a Bloom filter. See `/events`.++    The servers of a fleet all send the same events in the same order: those of+    the block and the mempool that the fleet shares, not what each server's own+    node saw. A transaction is told when it enters the shared mempool and when+    it leaves it unconfirmed, and a block when the fleet has moved to it, which+    is when every server can answer about it. A client can therefore reconnect+    to another server and ask it about what the first one told.++    ## Blockchain.info compatibility++    The endpoints under `/blockchain/` answer like those of Blockchain.info, with+    the same parameters, field names and conventions, so that software written+    for it can switch by changing its base URL. They are documented here where+    they differ or where the original is unclear. An operator may turn them off.++    ## Browsers++    Every response has the header `Access-Control-Allow-Origin: *`, so web+    applications can call the API directly from a browser. The API needs no+    credentials and keeps no sessions, so there is nothing for another site to+    abuse. A browser that first asks what it may send, with `OPTIONS`, is told:+    `GET`, `HEAD`, `POST` and `DELETE`, with the headers `Accept` and+    `Content-Type`.++    ## HTTP++    - **`HEAD`** works wherever `GET` does, and answers with the headers alone.+    - **Compression.** A request with `Accept-Encoding: gzip` gets its answer+      compressed, in either format. The events are not compressed: they have+      to leave as they happen.+    - **Long lists.** The endpoints that take a list of addresses, blocks,+      heights or transactions in a parameter take it under `POST` too, with+      the same parameters in a body of type+      `application/x-www-form-urlencoded`. A URL has room for some hundred+      addresses; a body has no such limit. `/transactions` is the exception:+      `POST` there publishes a transaction.+    - **`Vary: Accept, Accept-Encoding`** is on every answer, so that a cache+      keeps the formats apart.+    - **`Retry-After`** is on a 503: one second from a server that is behind+      its fleet, five from a health check that fails.+    - **The reference itself** is at the root of every server: `/` is this+      page, for the version that the server runs, and `/swagger.yaml` the file+      behind it.++    ## Binary format++    This section is enough to write a client in any language.++    ### Asking for it++    - Send the header `Accept: application/octet-stream`. In a list of types,+      binary is sent when that type has a `q` weight above 0 that is no less+      than the weight of `application/json`, if that is listed too. A+      wildcard such as `*/*`, or no header, gives JSON. There is no query+      parameter for it, and `pretty` has no effect on it.+    - Every answer has `Vary: Accept`, so that a cache keeps the two formats+      apart.+    - A binary answer has `Content-Type: application/octet-stream`. The body is+      one value, with nothing before or after it.+    - **An error comes in the format asked for**, like any other answer. In+      binary it is an `Except`, described below. Servers up to version 1.8.1+      sent every error as JSON: read the `Content-Type` of an error to tell.+    - `/health` answers 503 and 203 with a normal body, in the format asked for.+    - The only endpoint that reads a body is `POST /transactions`: one+      transaction as in the peer-to-peer protocol, as raw bytes or as hex. The+      server tells them apart by content, not by `Content-Type`.+    - The Blockchain.info endpoints and `/dbstats` have no binary form.+    - Over the WebSocket at `/events`, binary is binary frames: the server+      sends them when the handshake asked for binary, and from the first binary+      frame that the client sends.++    ### Primitives++    Everything is big-endian, except inside the peer-to-peer structures (`Tx`,+    `BlockHeader`, `OutPoint`, `Block`), which are byte for byte what nodes send.++    | Name | Encoding |+    | --- | --- |+    | `u8`, `u32`, `u64` | Unsigned integer of 1, 4 or 8 bytes, big-endian. |+    | `i32`, `i64` | Two's complement, big-endian. |+    | `u32le`, `u64le` | Little-endian; only in peer-to-peer structures. |+    | `bool` | One byte: `00` false, `01` true. |+    | `bytes` | `u64` length, then that many bytes. |+    | `text` | `bytes` that hold UTF-8. |+    | `list T` | `u64` count, then that many `T`. |+    | `opt T` | `00` for none; `01` then `T`. |+    | `map T` | `list` of pairs `u64` key, `T`; keys ascending, each once. |+    | `hash256` | 32 bytes, in the reverse order of the hex in JSON. |+    | `hash160` | 20 bytes, in the order of the hex. |+    | `varint` | Bitcoin's CompactSize, shortest form; only in peer-to-peer structures. |+    | `integer` | See below. |++    There are no variable-length integers outside the peer-to-peer structures:+    every length and count is a `u64`. A type with several forms starts with a+    tag byte, given in its table.++    `integer` holds a number of any size. `00` then `i32` when it fits 32 bits.+    Otherwise `01`, a sign byte (`01`, or `ff` for negative), a `u64` length,+    and the absolute value in that many bytes, least significant first, with no+    zero byte at the end.++    ### Peer-to-peer structures++    | Type | Layout |+    | --- | --- |+    | `OutPoint` | `hash256` transaction id, `u32le` output index. 36 bytes. |+    | `BlockHeader` | `u32le` version, `hash256` previous, `hash256` Merkle root, `u32le` time, bits, nonce. |+    | `Tx` | `u32le` version, `varint` count, inputs, `varint` count, outputs, `u32le` lock time. |+    | `Tx` input | `OutPoint`, `varint` length, script, `u32le` sequence. |+    | `Tx` output | `u64le` value, `varint` length, script. |+    | `Block` | `BlockHeader`, `varint` count, that many `Tx`. |++    A `Tx` with a witness has the SegWit form: `00 01` after the version, and+    before the lock time one stack per input, each a `varint` count of items,+    each a `varint` length and bytes. A `Tx` in which no input has a witness is+    written in the form without it, on every network.++    ### Address++    An address is a tag and a hash. It does not carry its network, so it decodes+    without knowing it. Writing it as text needs the network.++    | Tag | Then | Kind |+    | --- | --- | --- |+    | `00` | `hash160` | Pay to public key hash. |+    | `01` | `hash160` | Pay to script hash. |+    | `02` | `hash160` | Pay to witness public key hash. |+    | `03` | `hash256` | Pay to witness script hash. |+    | `04` | `u8` version, `bytes` program (40 at most) | Other SegWit address, such as Taproot. |+    | `05` | `hash256` | Pay to script hash of 32 bytes (BCH). |+    | `06` | `u8` type, `bytes` hash (64 at most) | Other CashAddr address. |++    The `hash256` of tags `03` and `05` is in the order of the script, not+    reversed.++    ### BlockRef and TxRef++    `BlockRef` says where a transaction is. Its numbers are **inverted**: each+    is written as the largest value of its width minus the number, so that the+    bytes sort newest first. Subtract again to read them.++    | Tag | Then | Meaning |+    | --- | --- | --- |+    | `00` | `u64`: 2^64 - 1 - time | In the mempool; time first seen. |+    | `01` | `u32`: 2^32 - 1 - height, `u32`: 2^32 - 1 - position | In a block, at that position in it. |++    `TxRef` is a `BlockRef`, then the `hash256` transaction id.++    ### Transaction++    | Field | Encoding | Note |+    | --- | --- | --- |+    | block | `BlockRef` | |+    | version | `u32` | |+    | locktime | `u32` | |+    | inputs | `list Input` | |+    | outputs | `list Output` | |+    | deleted | `bool` | |+    | rbf | `bool` | |+    | time | `u64` | Not inverted. |+    | txid | `hash256` | |+    | size | `u32` | With the witness. |+    | weight | `u32` | Present on every network. |+    | fee | `u64` | 0 for a coinbase. |++    `Input`, by tag:++    | Field | Encoding | `00` coinbase | `01` other |+    | --- | --- | --- | --- |+    | outpoint | `OutPoint` | yes | yes |+    | sequence | `u32` | yes | yes |+    | sigscript | `bytes` | yes | yes |+    | pkscript | `bytes` | | yes |+    | value | `u64` | | yes |+    | witness | `list bytes` | yes | yes |+    | address | `opt Address` | | yes |++    The witness list is present on every network, and empty where there is none.++    `Output`: `u64` value, `bytes` script, `opt Spender`, `opt Address`.++    `Spender`: `hash256` id of the spending transaction, `u32` input index.++    `TxData` is the record of a transaction in the database. No endpoint+    returns it; it is here for programs that read the database.++    | Field | Encoding | Note |+    | --- | --- | --- |+    | block | `BlockRef` | |+    | tx | `Tx` | |+    | prevs | `map Prev` | By input index. `Prev`: `bytes` script, `u64` value. |+    | deleted | `bool` | |+    | rbf | `bool` | |+    | time | `u64` | |+    | spenders | `map Spender` | By output index. |++    ### Blocks, balances and unspent outputs++    `BlockData`. The hash is not sent: it is the double SHA-256 of the header.++    | Field | Encoding | Note |+    | --- | --- | --- |+    | height | `u32` | |+    | mainchain | `bool` | |+    | work | `integer` | |+    | header | `BlockHeader` | |+    | size | `u32` | |+    | weight | `u32` | Present on every network. |+    | tx | `list hash256` | |+    | outputs | `u64` | |+    | fee | `u64` | |+    | subsidy | `u64` | |++    | Type | Layout |+    | --- | --- |+    | `Balance` | `Address`, then `u64` confirmed, unconfirmed, utxo, txs, received. |+    | `Unspent` | `BlockRef`, `OutPoint`, `u64` value, `bytes` script, `opt Address`. |+    | `XPubBal` | `list u32` path, `Balance`. |+    | `XPubUnspent` | `list u32` path, `Unspent`. |+    | `XPubSummary` | `u64` confirmed, unconfirmed, received, utxo; `u32` external, change. |+    | `TxId` | `hash256`. |+    | `Event` | Tag `00` new best block or `01` mempool transaction, then `hash256`. 33 bytes. |+    | `PeerInfo` | `bytes` user agent, `text` address, `i32` version, `u64` services, `bool` relay. |++    ### Commands of the event WebSocket++    What a client sends in a binary frame over the WebSocket at `/events`, one+    command to a frame: a tag, then what it carries.++    | Tag | Then | Command |+    | --- | --- | --- |+    | `00` | `list Address` | Watch these addresses too. |+    | `01` | `list Address` | Stop watching these addresses. |+    | `02` | `XPub`, `u8` derivation | Watch this extended public key too. |+    | `03` | `XPub`, `u8` derivation | Stop watching this key. |+    | `04` | `Filter` | Set the Bloom filter, in place of the one before. |+    | `05` | `bytes`, 520 at most | Add an element to the Bloom filter. |+    | `06` | | Remove the Bloom filter. |+    | `07` | | Forget all of it, and hear of every transaction. |+    | `08` | | Forget all of it, and hear of blocks alone. |++    `XPub` is the 78 bytes of the key as BIP 32 serializes it, which is what+    the Base58Check of its text decodes to: `u32` version of the network, `u8`+    depth, 4 bytes fingerprint of the parent, `u32` index, 32 bytes chain code,+    33 bytes compressed public key. The derivation is `00` legacy, `01` compat,+    `02` segwit.++    `Filter` is the `filterload` message of the peer-to-peer protocol (BIP 37):+    `varint` length and that many bytes of filter, 36,000 at most, `u32le`+    number of hash functions, 50 at most, `u32le` tweak, `u8` flags.++    ### Health++    Each part starts with its own `ok`, which repeats what its numbers say.++    | Field | Encoding | Note |+    | --- | --- | --- |+    | ok | `bool` | The whole check. |+    | blocks | `bool` ok, `u32` headers, `u32` blocks, `i32` max | |+    | last-block | `bool` ok, `i64` age, `i64` max | |+    | last-tx | `bool` ok, `i64` age, `i64` max | |+    | pending-txs | `bool` ok, `i64` count, `i64` max | |+    | peers | `bool` ok, `i64` count, `i64` min | |+    | net | `text` | |+    | version | `text` | |+    | time | `u64` | A number, not the text of the JSON. |+    | redis | `u8` | `00` no cache, `01` not answering, `02` answering. |++    Older servers end the body before `redis`; read that as no cache.++    ### What each endpoint returns++    Lists are `list T`. The events are the one exception: they have no count,+    because they have no end.++    An error, with any status that is not a success, is an `Except`: one byte+    that says which, and for some a body.++    | Byte | Error | Body |+    | --- | --- | --- |+    | `00` | `not-found-or-invalid-arg` | |+    | `01` | `server-error` | |+    | `02` | `bad-request` | |+    | `03` | `user-error` | The message: `bytes`, UTF-8. |+    | `04` | `string-error` | The message: `bytes`, UTF-8. |+    | `05` | `multiple-tx-index` | `list hash256`: the transactions that match. |+    | `06` | `server-timeout` | |+    | `07` | `request-too-large` | |+    | `08` | `behind-fleet` | |++    | Endpoints | Body |+    | --- | --- |+    | `/block/{block}`, `/block/best`, `/block/time/…`, `/block/mtp/…` | `BlockData` |+    | `/blocks`, `/block/latest`, `/block/height/…`, `/block/heights` | `list BlockData` |+    | `/transaction/{txid}` | `Transaction` |+    | `/transactions`, `/transactions/block/…`, `…/transactions/full` | `list Transaction` |+    | `/address/…/transactions`, `/xpub/…/transactions` | `list TxRef` |+    | `/address/{address}/balance` | `Balance` |+    | `/address/balances` | `list Balance` |+    | `/address/…/unspent` | `list Unspent` |+    | `/xpub/{xpub}` | `XPubSummary` |+    | `/xpub/{xpub}/balances` | `list XPubBal` |+    | `/xpub/{xpub}/unspent` | `list XPubUnspent` |+    | `DELETE /xpub/{xpub}` | `bool` |+    | `/transaction/{txid}/after/{height}` | `opt bool` |+    | `POST /transactions` | `TxId` |+    | `/mempool` | `list hash256` |+    | `/peers` | `list PeerInfo` |+    | `/health` | Health, as above. |+    | `…/raw` of one block or transaction | `Block` or `Tx`, bare. |+    | `…/raw` of several | `list Block` or `list Tx`. Servers up to 1.8.1 sent them with no count. |+    | `/events` | An endless stream of `Event`, **no count** and nothing between them. |++    The wrappers add no bytes: the `result` object of the JSON+    (`GenericResult`, `RawResult`, `RawResultList` in the Haskell client) is+    its content alone, and `SerialList` is `list T`.++tags:+  - name: Address+    description: |+      Balances, transactions and unspent outputs of addresses, one at a time or+      many at once, and of extended public keys (xpubs), which stand for every+      address of a wallet.+  - name: Transaction+    description: |+      Transactions by id, the transactions of a block, the mempool, and+      broadcasting new transactions to the network.+  - name: Block+    description: |+      Blocks by hash, height or time, the newest blocks, and their serialized+      form.+  - name: Network+    description: |+      The state of the server and the network: health, peers, and a live stream of+      new blocks and transactions.+  - name: Blockchain+    description: |+      Endpoints that answer like those of Blockchain.info, for software written+      for it. An operator may turn them off, in which case they answer 404.++paths:+  '/health':+    get:+      tags:+        - Network+      summary: Health Check+      description: |+        Whether this server is ready to answer, for load balancers and monitoring.++        The status is **200** when every check passes and **503** when one fails;+        the body is the same either way and says which check failed. A server that is+        still catching up with the network answers 503, so a load balancer stops+        sending it traffic until its answers are current.++        A server that has a Redis cache and cannot reach it answers **203** when+        every check passes, with the header `Haskoin-Degraded: redis` and+        `"redis": {"ok": false}` in the body. It goes on+        answering every request, from its own database: the answers are right for+        what this server has seen, but they are no longer the ones that the servers+        sharing that Redis give together, and requests about large wallets are+        slower. A load balancer that takes any status in the 200s for healthy keeps+        sending it traffic, which is what it should do; a monitor that expects 200+        sees that something is wrong.++        The checks are run in the background every 30 seconds (unless the operator+        changed it), and this endpoint returns the latest result. It does no work of+        its own, so it is safe to call as often as a load balancer likes.+      operationId: healthCheck+      parameters:+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '203':+          description: |+            Every check passes, but the server cannot reach its Redis and answers+            from its own database alone. The body is that of 200.+          headers:+            Haskoin-Degraded:+              description: What the server is doing without; `redis`.+              schema:+                type: string+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/HealthCheck'+        '200':+          description: success+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/HealthCheck'+              example:+                ok: true+                net: bch+                blocks:+                  ok: true+                  max: 2+                  blocks: 688794+                  diff: 0+                  headers: 688794+                pending-txs:+                  ok: true+                  max: 100+                  count: 0+                version: *version+                last-tx:+                  ok: true+                  max: 300+                  age: 0+                last-block:+                  ok: true+                  max: 7200+                  age: 3279+                peers:+                  ok: true+                  count: 20+                  min: 1+                time: '2021-06-14 10:21:43 UTC'+            application/octet-stream:+              schema:+                type: string+                format: binary+        '503':+          description: unavailable+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/HealthCheck'+              example:+                ok: false+                net: bch+                blocks:+                  ok: false+                  max: 2+                  blocks: 688794+                  diff: 4+                  headers: 688798+                pending-txs:+                  ok: true+                  max: 100+                  count: 0+                version: *version+                last-tx:+                  ok: true+                  max: 300+                  age: 0+                last-block:+                  ok: true+                  max: 7200+                  age: 3279+                peers:+                  ok: false+                  count: 0+                  min: 1+                time: '2021-06-14 10:21:43 UTC'++  '/address/{address}/transactions':+    get:+      tags:+        - Address+      summary: Transactions for Address+      description: |+        The transactions that pay to or spend from an address, newest first, as+        transaction ids with their place in the chain.++        This is the light way to follow an address: fetch the transactions you do not+        have yet with `/transactions`. Use `/transactions/full` to get them in one+        call when you need them all. See *Paging* at the top for `limit`, `offset`+        and `height`.++        An address with no transactions gives an empty list. An address that does+        not parse for this network gives 404.+      operationId: getAddressTxs+      parameters:+        - $ref: '#/components/parameters/addressParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Transaction list starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/TxRef'+              example:+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  block:+                    height: 688793+                    position: 3+                - txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                  block:+                    height: 688707+                    position: 786+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/{address}/transactions/full':+    get:+      tags:+        - Address+      summary: Detailed Address Transactions+      description: |+        Like `/address/{address}/transactions`, with the full transactions instead+        of their ids.++        Full transactions are much larger, so the most that one call returns is+        lower (5,000, unless the operator changed it).+      operationId: getAddressTxsFull+      parameters:+        - $ref: '#/components/parameters/addressParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Detailed transactions starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Transaction'+              example:+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  size: 225+                  version: 2+                  locktime: 688792+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                      output: 1+                      sigscript: "47304402206a11f253c8035be2d9f5958a9ca838b3fd5a1a120441885e10e6ca\+                                 d0fef3911f02202056263939a88fe06e7fc94368ff44c46f71e7daa603936d75\+                                 5b35b8b2a21c154121027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d\+                                 4c07b9f8c3d04327794c"+                      sequence: 4294967294+                      pkscript: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                      value: 2275183814+                      address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                      witness: []+                  outputs:+                    - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                      pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                      value: 15197686+                      spent: false+                      spender: null+                    - address: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                      pkscript: 76a9143ea707954f63a225fe4630701df0924c9e835a3488ac+                      value: 2259981628+                      spent: true+                      spender:+                        txid: 9e5fd342efb429df773d8208e3fde7af9b7440869e5963d761da0b317c4908ef+                        input: 0+                  block:+                    height: 688793+                    position: 3+                  deleted: false+                  time: 1621583361+                  rbf: false+                  weight: 900+                - txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                  size: 225+                  version: 2+                  locktime: 688706+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: 741f605735c995ab6d79814da49002f7b4eea973656e70410cbf7969ebf467fd+                      output: 0+                      sigscript: "4730440220298e397d155bcd0c3371a17f11d6dc06392c72c6d4646ac27fad72\+                                 e86e7f18f50220471c7338a0f4573955e52a95c4c2717f5ae5d369ad153ae627\+                                 49d0dcd9250482412103c60a0fd16eaf3c72dee4bf12a566ee3a6ae181a29b66\+                                 a1f2407442f7a6d39ad2"+                      sequence: 4294967294+                      pkscript: 76a9145124c4e68eed7ffc6110c2326d7d3739f55a09eb88ac+                      value: 2775188314+                      address: bitcoincash:qpgjf38x3mkhllrpzrprymtaxuul2ksfavle5hmhl4+                      witness: []+                  outputs:+                    - address: bitcoincash:qqx0ende3cx70rdsmml6ap5pcc9nqmns5qlc8yvhga+                      pkscript: 76a9140cfccdb98e0de78db0deffae8681c60b306e70a088ac+                      value: 500000000+                      spent: false+                      spender: null+                    - address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                      pkscript: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                      value: 2275183814+                      spent: true+                      spender:+                        txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                        input: 0+                  block:+                    height: 688707+                    position: 786+                  deleted: false+                  time: 1621533785+                  rbf: false+                  weight: 900+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/transactions':+    get:+      tags:+        - Address+      summary: Multiple Address Transactions+      description: |+        The transactions of several addresses, merged into one list, newest first.+        A transaction that touches several of the addresses appears once.++        This is how to follow a wallet that is a list of addresses rather than an+        xpub: one call, and one consistent page, instead of one call per address.+      operationId: getMultipleAddressTxs+      parameters:+        - $ref: '#/components/parameters/addressesParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of transactions starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/TxRef'+              example:+                - txid: 9e5fd342efb429df773d8208e3fde7af9b7440869e5963d761da0b317c4908ef+                  block:+                    height: 688793+                    position: 458+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  block:+                    height: 688793+                    position: 3+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  block:+                    height: 688793+                    position: 3+                - txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                  block:+                    height: 688707+                    position: 786+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/transactions/full':+    get:+      tags:+        - Address+      summary: Multiple Address Detailed Transactions+      description: |+        Like `/address/transactions`, with the full transactions instead of their+        ids, and the lower maximum of full transactions.+      operationId: getMultipleAddressTxsFull+      parameters:+        - $ref: '#/components/parameters/addressesParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Detailed transactions starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Transaction'+              example:+                - txid: 9e5fd342efb429df773d8208e3fde7af9b7440869e5963d761da0b317c4908ef+                  size: 225+                  version: 2+                  locktime: 688792+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                      output: 1+                      sigscript: "4730440220362587be41ff4ca25ef097b6c4229b031c26420f53143560fc8bf2\+                                 4f65418dac022034b0c97cef1ff51460346daca727f926ee675e1acaf63cb092\+                                 a8a39cb3b5d8f94121029832be5ec435a55cd8c09474348cf7ffdcf266ae4fc8\+                                 8f390bde21d9a9c60d09"+                      sequence: 4294967294+                      pkscript: 76a9143ea707954f63a225fe4630701df0924c9e835a3488ac+                      value: 2259981628+                      address: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                      witness: []+                  outputs:+                    - address: bitcoincash:qpfscnfvsaydyru7wajy42lqumvpf7drzgc7wz87sj+                      pkscript: 76a914530c4d2c8748d20f9e77644aabe0e6d814f9a31288ac+                      value: 1400000000+                      spent: false+                      spender: null+                    - address: bitcoincash:qrw8u9tctjl4ndggrl6l9ssam4qdse3agynldjdn48+                      pkscript: 76a914dc7e15785cbf59b5081ff5f2c21ddd40d8663d4188ac+                      value: 859977128+                      spent: true+                      spender:+                        txid: 03b59d0fe4bb260587656f57a2f42a34e97f6502ffb7ee93b8e58f6f8933c99a+                        input: 0+                  block:+                    height: 688793+                    position: 458+                  deleted: false+                  time: 1621583381+                  rbf: false+                  weight: 900+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  size: 225+                  version: 2+                  locktime: 688792+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                      output: 1+                      sigscript: "47304402206a11f253c8035be2d9f5958a9ca838b3fd5a1a120441885e10e6ca\+                                 d0fef3911f02202056263939a88fe06e7fc94368ff44c46f71e7daa603936d75\+                                 5b35b8b2a21c154121027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d\+                                 4c07b9f8c3d04327794c"+                      sequence: 4294967294+                      pkscript: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                      value: 2275183814+                      address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                      witness: []+                  outputs:+                    - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                      pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                      value: 15197686+                      spent: false+                      spender: null+                    - address: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                      pkscript: 76a9143ea707954f63a225fe4630701df0924c9e835a3488ac+                      value: 2259981628+                      spent: true+                      spender:+                        txid: 9e5fd342efb429df773d8208e3fde7af9b7440869e5963d761da0b317c4908ef+                        input: 0+                  block:+                    height: 688793+                    position: 3+                  deleted: false+                  time: 1621583361+                  rbf: false+                  weight: 900+                - txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  size: 225+                  version: 2+                  locktime: 688792+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                      output: 1+                      sigscript: "47304402206a11f253c8035be2d9f5958a9ca838b3fd5a1a120441885e10e6ca\+                                 d0fef3911f02202056263939a88fe06e7fc94368ff44c46f71e7daa603936d75\+                                 5b35b8b2a21c154121027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d\+                                 4c07b9f8c3d04327794c"+                      sequence: 4294967294+                      pkscript: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                      value: 2275183814+                      address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                      witness: []+                  outputs:+                    - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                      pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                      value: 15197686+                      spent: false+                      spender: null+                    - address: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                      pkscript: 76a9143ea707954f63a225fe4630701df0924c9e835a3488ac+                      value: 2259981628+                      spent: true+                      spender:+                        txid: 9e5fd342efb429df773d8208e3fde7af9b7440869e5963d761da0b317c4908ef+                        input: 0+                  block:+                    height: 688793+                    position: 3+                  deleted: false+                  time: 1621583361+                  rbf: false+                  weight: 900+                - txid: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                  size: 225+                  version: 2+                  locktime: 688706+                  fee: 4500+                  inputs:+                    - coinbase: false+                      txid: 741f605735c995ab6d79814da49002f7b4eea973656e70410cbf7969ebf467fd+                      output: 0+                      sigscript: "4730440220298e397d155bcd0c3371a17f11d6dc06392c72c6d4646ac27fad72\+                                 e86e7f18f50220471c7338a0f4573955e52a95c4c2717f5ae5d369ad153ae627\+                                 49d0dcd9250482412103c60a0fd16eaf3c72dee4bf12a566ee3a6ae181a29b66\+                                 a1f2407442f7a6d39ad2"+                      sequence: 4294967294+                      pkscript: 76a9145124c4e68eed7ffc6110c2326d7d3739f55a09eb88ac+                      value: 2775188314+                      address: bitcoincash:qpgjf38x3mkhllrpzrprymtaxuul2ksfavle5hmhl4+                      witness: []+                  outputs:+                    - address: bitcoincash:qqx0ende3cx70rdsmml6ap5pcc9nqmns5qlc8yvhga+                      pkscript: 76a9140cfccdb98e0de78db0deffae8681c60b306e70a088ac+                      value: 500000000+                      spent: false+                      spender: null+                    - address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                      pkscript: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                      value: 2275183814+                      spent: true+                      spender:+                        txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                        input: 0+                  block:+                    height: 688707+                    position: 786+                  deleted: false+                  time: 1621533785+                  rbf: false+                  weight: 900+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/{address}/balance':+    get:+      tags:+        - Address+      operationId: getBalance+      summary: Address Balance+      description: |+        The balance of an address: confirmed and unconfirmed amounts, the number of+        unspent outputs and of transactions, and the total ever received.++        An address that has never been used has a balance of zero rather than giving+        404: every valid address exists, it just has not received anything yet.+      parameters:+        - $ref: '#/components/parameters/addressParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Address balance and other stats.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Balance'+              example:+                address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                confirmed: 0+                unconfirmed: 0+                utxo: 0+                txs: 2+                received: 2275183814+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/balances':+    get:+      tags:+        - Address+      summary: Multiple Address Balances+      description: |+        The balances of several addresses, in one call. Every address in the request+        has a balance in the response, unused ones with zeros.+      operationId: getMultipleAddressBalances+      parameters:+        - $ref: '#/components/parameters/addressesParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Address balances and other stats.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Balance'+              example:+                - address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                  confirmed: 0+                  unconfirmed: 0+                  utxo: 0+                  txs: 2+                  received: 2275183814+                - address: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                  confirmed: 0+                  unconfirmed: 0+                  utxo: 0+                  txs: 2+                  received: 2259981628+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid Address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/{address}/unspent':+    get:+      tags:+        - Address+      summary: Address Unspent Outputs+      description: |+        The outputs paid to an address that are not spent yet, newest first: the+        coins a wallet can spend.++        Outputs spent by an unconfirmed transaction are left out, because spending+        them again would conflict with it. Unconfirmed outputs are included, with+        `"block": {"mempool": …}`; whether to spend them is the wallet's choice.+      operationId: getAddressUnspent+      parameters:+        - $ref: '#/components/parameters/addressParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of unspent outputs starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Unspent'+              example:+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688793+                    position: 3+                  txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 15197686+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688664+                    position: 51+                  txid: 1c9337a7bad182b5cf303b6c3ad7aeac8cbc162dee11ace30671d12d83f80501+                  index: 1+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 28288039+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688601+                    position: 3820+                  txid: c41e77bb8ab9f78a0ca26c43ba8a9e687510ccc303a9637e86bfb8affcd1dcf6+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 42056933+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 686234+                    position: 3648+                  txid: d4cad6861428215ed6f86406eb445305b107ab041f02e7ef5879381e9cc11ec0+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 24597765+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/address/unspent':+    get:+      tags:+        - Address+      summary: Multiple Address Unspent Outputs+      description: |+        The unspent outputs of several addresses, merged into one list, newest+        first.+      operationId: getMultipleAddressUnspent+      parameters:+        - $ref: '#/components/parameters/addressesParam'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of unspent outputs starting with most recent.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Unspent'+              example:+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688793+                    position: 3+                  txid: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 15197686+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688664+                    position: 51+                  txid: 1c9337a7bad182b5cf303b6c3ad7aeac8cbc162dee11ace30671d12d83f80501+                  index: 1+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 28288039+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 688601+                    position: 3820+                  txid: c41e77bb8ab9f78a0ca26c43ba8a9e687510ccc303a9637e86bfb8affcd1dcf6+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 42056933+                - address: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                  block:+                    height: 686234+                    position: 3648+                  txid: d4cad6861428215ed6f86406eb445305b107ab041f02e7ef5879381e9cc11ec0+                  index: 0+                  pkscript: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                  value: 24597765+                - address: bitcoincash:qp5p2rhvl6dtl0jkvrzf2w7u3fxpylu3ks7wh3vtlp+                  block:+                    height: 685236+                    position: 15161+                  txid: f80be09fecde882e3b59e39f05f93b51ed92dfe099ee24384b6f30d1d324a995+                  index: 0+                  pkscript: 76a91468150eecfe9abfbe5660c4953bdc8a4c127f91b488ac+                  value: 52128096+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/best':+    get:+      tags:+        - Block+      summary: Block Chain Head+      description: |+        The newest block of the chain with the most work that this server has+        imported.++        It can be a block or two behind the network while the server imports a new+        one; `/health` says how far behind it is.+      operationId: getBlockHead+      parameters:+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Best block data.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BlockData'+              example:+                hash: 000000000000000001b64b6325acf226dd0b9c012c1d126917bf444f35408961+                height: 688797+                mainchain: true+                previous: 0000000000000000005ed0fb2297eee123e8970060ed52ce021ce415b3d21e0b+                time: 1621589645+                version: 805298176+                bits: 402828819+                nonce: 1843321275+                size: 14345+                tx:+                  - a12f364c11eeb9f5b48295ed52de6d62246d14db884fde68510489ef8afe36dd+                  - 0426d4a7d9635f805d657823395ba7f88c28391015c8ff7166f136d4cb1e7003+                  - 078169b239fb052fd11947ca2e06d72bcaf2dcd8e2ca2de3641a99972da555aa+                  - 09e1a01f75cc41d3a6ddd33cfc6c7c43cff546b6017c42098dc42c0380894d31+                  - 1706b23e11d5e65a6b5a7e5b2883be7601037865c46db872b56cce959f25584b+                  - 2638456e1b6874b978dbfa545b56e698344d5d776c7fcf9fd283f6ba7eeb0f2d+                  - 2d11fe4dc71ffb9b3245927fb8a3821a440d555a2a7cdcba1b561be7d4d5851a+                  - 2f4458e9b637b3be97db4c896ef6e396d4a4502e4c00493b8b12e4e672ff991d+                  - 30adc4c7e8fc46b2756d7edd4c3ca13ba08d24ba1cff99e4cb59864b442d1b56+                  - 30ffe61ae7d106b00a8a3a8b5195ddd5291deb0120fdc620fa42689b481923fa+                  - 3166134ffddf6fe6cb816aceb59894274ffeea320406876d8e77b1afa4c98056+                  - 3ba5dbc9ab73ef827898c624c7137f41f960efd6e06fb149f67a899731db831d+                  - 3f7a69e8104aa103e0988a0201ac2d40347ed84ab4c3ed239ddbe079d269dae8+                  - 3fb631d18bd4b86aa2d5156f01a69a89a958b9999cbad2abafe55b65d917c812+                  - 4cadeaaf483ea263b8a5a26cfcf227795ec0e468400d16067163b0a20d4b0834+                  - 51ebe726bdb690d70ad331e4fb08dddcdd7eaa1a45d8fcc0ac8cee56f54f0d72+                  - 613229215ee961c1ec452685d2031f435ca65e1d96744615e590b87ebeb2828a+                  - 6243831873230e731091c49d86d65de17cd5a9218d71a1e83213908d532c0014+                  - 6ce70f7d51b0bb80c3ca6c84bb1131807c3316c760e080fdbd296cc4f319d097+                  - 734fc62db583495c14b14ffccc12f89b2cea3e8d179e07c78ad6cd5746b018b7+                  - 777981b9cf7e9222d33129e511c4c247883052622f44e2c61b0393018ba111af+                  - 7985ef5d0ab5e7ff7791dd3d82db1673f66f1b9c0d95d27628025253497ca22e+                  - 7c071a25ffa5e6333e65dcc86460c25a5ef41d7502603beabdc0541aef18d782+                  - 7d237bf9a123281edebebd7029d77b4c7726df54f330ae1228359897b976ef11+                  - 884f75299f7dc57cb9844f88b0752b8522c128a0d4915084a0a844cec538a38f+                  - 9cb79e1b11c6bdea8ccab3f61411679bd5b19c17651fb71e0d9dbef04fa27d09+                  - b7596e008884c68a0322c56ccd94c429613e6be8c958e5c0c93ced6915d32c86+                  - be9e621846769a6a92c68882e0b5eb6352a3be3469e4d9beb3926b64039e1037+                  - c186585d8e4cae9f04a6c2cb463c79f5f78331434c5bed3c5616672a06919b91+                  - c6fc1a6d92877b9878ab08169c22ce39028c3e368d53544811f74f42cdc9e138+                  - c8421314ebfe5a312066c2e0feb00eacf49fd28ca174c89461eabdbea5a7fdd5+                  - d43a179f02356f62b8d42736c4d70da7584f3c1bbc29d8b2fbe147b9f89ebc04+                  - d6312a20c5df4368e975438556aac9db816d261d03c57c3b831f13fa40d1a303+                  - d7dc9b223ea0c2e21877611afe65df412164d5a08d2446763dda5bca72719c31+                  - df562b911ee84726e9be7c2496aa60466f34a6d748c47f368a05cbd29f45aaca+                  - e3b023c0cf9554dfb2ece828dfd94cce4438bfd28c57dfc62af843f9c09cbb4c+                  - e9ef985c16a176c1ea3890edcf8fab187703356008016b3aa252844100dafa51+                  - ed306dfe986b37aa64006e7deeb5c2342b563b4ca130de53b1e6cb6c8c57758a+                  - edf9f11512782dab3e6e4efc47f0491f30ffb87d005f78dd623bae027fb92579+                  - f36b81e3aa3340d03a50f7cf71a4c4cda91cf972198a508ecb32c9c41feb7773+                  - ff85614565dcc8710dc4f6a96b64ea593e30e0b437980d3702e4adf99aab3f87+                merkle: f379f45cb00f781a94e5ea4be0c194c4d9715c374d02ec1494800869bfd542b8+                subsidy: 625000000+                fees: 87081+                outputs: 115319020328+                work: 440514608165117083892886234+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/block/best/raw':+    get:+      tags:+        - Block+      summary: Raw Block Chain Head+      description: |+        The newest block, serialized as in the peer-to-peer protocol, with all its+        transactions.+      operationId: getRawBlockHead+      parameters:+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol block.+            Hex string when using JSON.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: string+                    example: "010000006f187fddd5e28aa1b4065daa5d9eae0c487094fb20cf97ca02b81c84\+                             000000005b7b25b51797f83192f9fd2c3871bfb27570a7d6b56d3a50760613d1\+                             a2fc1aeeab346849ffff001d36d9507101010000000100000000000000000000\+                             00000000000000000000000000000000000000000000ffffffff0704ffff001d\+                             0120ffffffff0100f2052a0100000043410408ab2f56361f83064e4ce51acc29\+                             1fb57c2cbcdb1d6562f6278c43a1406b548fd6cefc11bcc29eb620d5861cb9ed\+                             69dc39f2422f54b06a8af4f78c8276cfdc6bac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/block/latest':+    get:+      tags:+        - Block+      summary: Most Recent Blocks+      description: |+        The newest block and the ones before it, up to 100 blocks, newest first. For+        explorers that show recent blocks; use `notx=true` when you do not need the+        transaction ids, as blocks can have thousands of them.+      operationId: getLatestBlocks+      parameters:+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of most recent blocks.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BlockData'+              example:+                - hash: 000000000000000001b64b6325acf226dd0b9c012c1d126917bf444f35408961+                  height: 688797+                  mainchain: true+                  previous: 0000000000000000005ed0fb2297eee123e8970060ed52ce021ce415b3d21e0b+                  time: 1621589645+                  version: 805298176+                  bits: 402828819+                  nonce: 1843321275+                  size: 14345+                  tx:+                    - a12f364c11eeb9f5b48295ed52de6d62246d14db884fde68510489ef8afe36dd+                  merkle: f379f45cb00f781a94e5ea4be0c194c4d9715c374d02ec1494800869bfd542b8+                  subsidy: 625000000+                  fees: 87081+                  outputs: 115319020328+                  work: 440514608165117083892886234+                - hash: 0000000000000000005ed0fb2297eee123e8970060ed52ce021ce415b3d21e0b+                  height: 688796+                  mainchain: true+                  previous: 000000000000000000f3e8c40d8fcb08fe68309d05c654d4a9626e71771c87bc+                  time: 1621589661+                  version: 536870912+                  bits: 402828953+                  nonce: 4121941127+                  size: 466349+                  tx:+                    - 1be65880d02647bc4b8b5525b6dd33848ac4f36cc2c682a35676d0c5e10858c2+                  merkle: a42b2321e225dd3812f1f1e502ea320eff30d9061f4089613250d5e6d9f3f1ca+                  subsidy: 625000000+                  fees: 2623401+                  outputs: 5682996095523+                  work: 440512846073222977677902178+                - hash: 000000000000000000f3e8c40d8fcb08fe68309d05c654d4a9626e71771c87bc+                  height: 688795+                  mainchain: true+                  previous: 000000000000000000e2bd91f8f073c8a755335a1e16bd99d21d9a52497f80ca+                  time: 1621589252+                  version: 541065216+                  bits: 402826569+                  nonce: 4143833738+                  size: 3789642+                  tx:+                    - 09aecab0edb71bbd37d2482948ddc6bc3475b6723ed485620ef926571ab6af04+                  merkle: df42f51b831eda9ddcf84de248fa7fed56cec66a9f15545c9a0802f5e2ab1dd4+                  subsidy: 625000000+                  fees: 9075725+                  outputs: 58906363954005+                  work: 440511085324684718115244800+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/block/{block}':+    get:+      tags:+        - Block+      operationId: getBlock+      summary: Block Information+      description: |+        A block by its hash, with the ids of its transactions.++        Blocks that are no longer in the main chain after a reorganisation keep+        answering, with `"mainchain": false`, so a link to a block never breaks.+      parameters:+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/blockParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Block data.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BlockData'+              example:+                hash: 000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506+                height: 100000+                mainchain: true+                previous: 000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250+                time: 1293623863+                version: 1+                bits: 453281356+                nonce: 274148111+                size: 957+                tx:+                  - 8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87+                  - fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4+                  - 6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4+                  - e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d+                merkle: f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766+                subsidy: 5000000000+                fees: 0+                outputs: 5301000000+                work: 451709610344319134+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/{block}/raw':+    get:+      tags:+        - Block+      operationId: getRawBlock+      summary: Raw Block+      description: |+        A block by its hash, serialized as in the peer-to-peer protocol, with all+        its transactions.+      parameters:+        - $ref: '#/components/parameters/blockParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol block.+            Hex string when using JSON.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: string+                    example: "0100000050120119172a610421a6c3011dd330d9df07b63616c2cc1f1cd00200\+                             000000006657a9252aacd5c0b2940996ecff952228c3067cc38d4885efb5a4ac\+                             4247e9f337221b4d4c86041b0f2b571004010000000100000000000000000000\+                             00000000000000000000000000000000000000000000ffffffff08044c86041b\+                             020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79\+                             a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d08\+                             7fedf9ce467cb9f7c6287078f801df276fdf84ac000000000100000001032e38\+                             e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88fdf357a187000000\+                             008c493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57d\+                             a93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1\+                             ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6c\+                             a164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f\+                             25f87c161bc6f8a630121df2b3d3ffffffff0200e32321000000001976a914c3\+                             98efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe208010000001976a9\+                             14948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac000000000100000001\+                             c33ebff2a709f13d9f9a7569ab16a32786af7d7e2de09265e41c61d078294ecf\+                             010000008a4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27\+                             942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe21\+                             9a886b84269eaeb81e26b4fe014104ae31c31bf91278d99b8377a35bbce5b27d\+                             9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b9\+                             0da4631ee39560639db462e9cb850fffffffff0240420f00000000001976a914\+                             b0dcbf97eabf4404e31d952477ce822dadbe7e1088acc060d211000000001976\+                             a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac0000000001000000\+                             010b6072b386d4a773235237f64c1126ac3b240c84b917a3909ba1c43ded5f51\+                             f4000000008c493046022100bb1ad26df930a51cce110cf44f7a48c3c561fd97\+                             7500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb46\+                             5f3e6f8da12e2c5303954aca7f78f3014104a7135bfe824c97ecc01ec7d7e336\+                             185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c24dd094d4\+                             2fdbfdd5aad3e063ce6af4cfaaea4ea14fbbffffffff0140420f000000000019\+                             76a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blocks':+    get:+      tags:+        - Block+      operationId: getBlocks+      summary: Information for Multiple Blocks+      description: |+        Several blocks by hash, in one call. Hashes that are not known are left out+        of the response rather than failing the whole request, and repeated hashes+        are answered once.+      parameters:+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/blocksParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of block data objects.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BlockData'+              example:+                - hash: 000000000845517b31c6820d83f25cff46429bf136a7515fe504116427e60f8e+                  height: 49999+                  mainchain: true+                  previous: 000000001a793b5b7732ee306b1e4196132e0193fac6457801f1e16e6ec89e6a+                  time: 1270916354+                  version: 1+                  bits: 472518933+                  nonce: 64497466+                  size: 216+                  tx:+                    - 2c95132eb2fd2e7d2f5bd282595d3d47950836031727850579dca836254b1bd4+                  merkle: 2c95132eb2fd2e7d2f5bd282595d3d47950836031727850579dca836254b1bd4+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 363097871874800+                - hash: 000000001aeae195809d120b5d66a39c83eb48792e068f8ea1fea19d84a4278a+                  height: 50000+                  mainchain: true+                  previous: 000000000845517b31c6820d83f25cff46429bf136a7515fe504116427e60f8e+                  time: 1270916538+                  version: 1+                  bits: 472518933+                  nonce: 58489010+                  size: 216+                  tx:+                    - 27f1d66f8a1ee5280f4e92508dcb647e954d53004905d08a75574daee4988360+                  merkle: 27f1d66f8a1ee5280f4e92508dcb647e954d53004905d08a75574daee4988360+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 363124009197915+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/block/height/{height}':+    get:+      tags:+        - Block+      operationId: getBlocksAtHeight+      summary: Blocks at Height+      description: |+        The blocks at a height. It is a list because the server keeps the blocks of+        chains that were abandoned in a reorganisation; the one in the main chain+        has `"mainchain": true`.+      parameters:+        - $ref: '#/components/parameters/blockHeightParam'+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Block data objects including orphans.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BlockData'+              example:+                - hash: 000000008f1a7008320c16b8402b7f11e82951f44ca2663caf6860ab2eeef320+                  height: 200+                  mainchain: true+                  previous: 00000000b7691ccc084542565697eca256e56bb7f67e560b48789db27f0468eb+                  time: 1231753120+                  version: 1+                  bits: 486604799+                  nonce: 1832536838+                  size: 215+                  tx:+                    - 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                  merkle: 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 863301599433+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/height/{height}/raw':+    get:+      tags:+        - Block+      operationId: getRawBlocksAtHeight+      summary: Raw Blocks at Height+      description: |+        The blocks at a height, serialized as in the peer-to-peer protocol.+      parameters:+        - $ref: '#/components/parameters/blockHeightParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol blocks.+            Hex strings when using JSON.+          content:+            application/json:+              schema:+                type: array+                items:+                  type: string+                  example: "01000000eb68047fb29d78480b567ef6b76be556a2ec975656424508cc1c69b7\+                           00000000bad58718fc3c6f5474918f06c44400c70b4c86d55a3f3ca3493b1d40\+                           c2061f2ba00f6b49ffff001d064b3a6d01010000000100000000000000000000\+                           00000000000000000000000000000000000000000000ffffffff0704ffff001d\+                           0138ffffffff0100f2052a010000004341045e071dedd1ed03721c6e9bba28fc\+                           276795421a378637fb41090192bb9f208630dcbac5862a3baeb9df3ca6e4e256\+                           b7fd2404824c20198ca1b004ee2197866433ac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/time/{time}':+    get:+      tags:+        - Block+      operationId: getBlockAtTime+      summary: Block Mined at Time+      description: |+        The last block of the main chain whose timestamp is at or before a time:+        the block that was the newest at that moment, as far as the timestamps of+        blocks tell.++        Timestamps of blocks are set by miners and are only roughly right; they can+        even go backwards from one block to the next. For a time that only goes+        forward, use `/block/mtp/{time}`.+      parameters:+        - $ref: '#/components/parameters/timeParam'+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Last block mined at or before specified time.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BlockData'+              example:+                hash: 000000008f1a7008320c16b8402b7f11e82951f44ca2663caf6860ab2eeef320+                height: 200+                mainchain: true+                previous: 00000000b7691ccc084542565697eca256e56bb7f67e560b48789db27f0468eb+                time: 1231753120+                version: 1+                bits: 486604799+                nonce: 1832536838+                size: 215+                tx:+                  - 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                merkle: 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                subsidy: 5000000000+                fees: 0+                outputs: 0+                work: 863301599433+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/time/{time}/raw':+    get:+      tags:+        - Block+      operationId: getRawBlockAtTime+      summary: Raw Block Mined at Time+      description: |+        Like `/block/time/{time}`, serialized as in the peer-to-peer protocol.+      parameters:+        - $ref: '#/components/parameters/timeParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol block mined at or before specified time.+            Hex string when using JSON.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: string+                    example: "0100000000000000000000000000000000000000000000000000000000000000\+                             000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa\+                             4b1e5e4a29ab5f49ffff001d1dac2b7c01010000000100000000000000000000\+                             00000000000000000000000000000000000000000000ffffffff4d04ffff001d\+                             0104455468652054696d65732030332f4a616e2f32303039204368616e63656c\+                             6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f75742066\+                             6f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe554827\+                             1967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4\+                             f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/mtp/{time}':+    get:+      tags:+        - Block+      operationId: getBlockMTP+      summary: Block at Median Time Past+      description: |+        The first block of the main chain whose median time past is at or after a+        time.++        The median time past of a block is the median of the timestamps of the 11+        blocks up to it. Unlike the timestamps themselves, it never goes backwards,+        and it is the time that the consensus rules use for time locks, so it is the+        right one to answer "from which block on is this lock time reached".+      parameters:+        - $ref: '#/components/parameters/timeParam'+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Block at Median Time Past+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BlockData'+              example:+                hash: 000000007986095d6dce2f41fd76e8008e828282400e46d3b49f39d19e2b7a78+                height: 206+                mainchain: true+                previous: 00000000d7e3261b16abe2fc1811150812ee0d6f6fc3727cadd8821df2d96c45+                time: 1231757577+                version: 1+                bits: 486604799+                nonce: 125825145+                size: 215+                tx:+                  - 32cbb7e4d08f02fdd78368a7a03ff54c13191d1023aa1e3f157027241d9840ae+                merkle: 32cbb7e4d08f02fdd78368a7a03ff54c13191d1023aa1e3f157027241d9840ae+                subsidy: 5000000000+                fees: 0+                outputs: 0+                work: 889071796431+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/mtp/{time}/raw':+    get:+      tags:+        - Block+      operationId: getRawBlockMTP+      summary: Raw Block at Median Time Past+      description: |+        Like `/block/mtp/{time}`, serialized as in the peer-to-peer protocol.+      parameters:+        - $ref: '#/components/parameters/timeParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol block mined after specified median time past.+            Hex string when using JSON.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: string+                    example: "01000000456cd9f21d82d8ad7c72c36f6f0dee1208151118fce2ab161b26e3d7\+                             00000000ae40981d242770153f1eaa23101d19134cf53fa0a76883d7fd028fd0\+                             e4b7cb3209216b49ffff001d79f07f0701010000000100000000000000000000\+                             00000000000000000000000000000000000000000000ffffffff0704ffff001d\+                             010effffffff0100f2052a01000000434104bc184a82d824a8ff59576e8b6843\+                             767407e4f35edb5ad725460764a30ddf91479fd0eb1b0c2b4371a06b3fdae345\+                             3262fcf3358afafe3dbebccf2fdefcd0853cac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/block/heights':+    get:+      tags:+        - Block+      operationId: getBlocksAtHeights+      summary: Blocks at Multiple Heights+      description: |+        The blocks at several heights, in one call, all of them for each height.+      parameters:+        - $ref: '#/components/parameters/blockHeightsParam'+        - $ref: '#/components/parameters/notxParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of block data objects at specified heights.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BlockData'+              example:+                - hash: 000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a+                  height: 100+                  mainchain: true+                  previous: 00000000cd9b12643e6854cb25939b39cd7a1ad0af31a9bd8b2efe67854b1995+                  time: 1231660825+                  version: 1+                  bits: 486604799+                  nonce: 1573057331+                  size: 215+                  tx:+                    - 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866+                  merkle: 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 433798316133+                - hash: 000000008f1a7008320c16b8402b7f11e82951f44ca2663caf6860ab2eeef320+                  height: 200+                  mainchain: true+                  previous: 00000000b7691ccc084542565697eca256e56bb7f67e560b48789db27f0468eb+                  time: 1231753120+                  version: 1+                  bits: 486604799+                  nonce: 1832536838+                  size: 215+                  tx:+                    - 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                  merkle: 2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 863301599433+                - hash: 0000000062b69e4a2c3312a5782d7798b0711e9ebac065cd5d19f946439f8609+                  height: 300+                  mainchain: true+                  previous: 00000000b9b003c04b2fe21b4f7a2fa809eb643852573c3a1748653e2019f01c+                  time: 1231832435+                  version: 1+                  bits: 486604799+                  nonce: 951842076+                  size: 215+                  tx:+                    - bc15f9dcbe637c187bb94247057b14637316613630126fc396c22e08b89006ea+                  merkle: bc15f9dcbe637c187bb94247057b14637316613630126fc396c22e08b89006ea+                  subsidy: 5000000000+                  fees: 0+                  outputs: 0+                  work: 1292804882733+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transaction/{txid}':+    get:+      tags:+        - Transaction+      operationId: getTransaction+      summary: Detailed Transaction+      description: |+        A transaction, with what this server knows about it beyond its raw form:+        where it is in the chain, the fee, the address and amount of every output it+        spends, and which transaction spends each of its outputs.++        Unconfirmed transactions are here too. A transaction that was removed from+        the mempool, or whose block was abandoned, is not found.+      parameters:+        - $ref: '#/components/parameters/txidParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Detailed transaction object.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Transaction'+              example:+                txid: 03c16602a41ae7cbc2a25b0d2801fdc3295e2e8ebfb26774030b2f7d538f5568+                size: 219+                version: 2+                locktime: 0+                fee: 220+                inputs:+                  - coinbase: false+                    txid: 02ea64dfff60b1f215282bbb4b2d729fecc50d81d2d0ef09a60d550a5a69418f+                    output: 1+                    sigscript: "41e3dc2001cffff05e62751c071ac05dee8511b9af626fe5b07bf271ce48df66\+                               cf4013d4b4b280bb74eac6206d772aa24f1e766bc813dc68a461736392ce97f8\+                               a04121028d75bdff57fcecff68f2927092b15074a4524a8d8182b6de761f6af3\+                               b8c6ba8f"+                    sequence: 0+                    pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                    value: 8985694+                    address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                    witness: []+                outputs:+                  - address: bitcoincash:qrnltz5afhaxlh6grupll8nfdswpatwnvcsyucwcle+                    pkscript: 76a914e7f58a9d4dfa6fdf481f03ff9e696c1c1eadd36688ac+                    value: 1230+                    spent: false+                    spender: null+                  - address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                    pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                    value: 8984244+                    spent: true+                    spender:+                      txid: 4670c8b46211c06aa1a1c42e8c7e090e971c2549d30aac65cdb12876782c2248+                      input: 0+                block:+                  height: 688804+                  position: 13+                deleted: false+                time: 1621593351+                rbf: false+                weight: 876+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Transaction not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transaction/{txid}/raw':+    get:+      tags:+        - Transaction+      operationId: getRawTransaction+      summary: Raw Transaction+      description: |+        A transaction serialized as in the peer-to-peer protocol, as a node would+        send it.+      parameters:+        - $ref: '#/components/parameters/txidParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol transaction.+            Hex string if using JSON.+          content:+            application/json:+              schema:+                type: string+                example: "02000000018f41695a0a550da609efd0d2810dc5ec9f722d4bbb2b2815f2b160\+                         ffdf64ea02010000006441e3dc2001cffff05e62751c071ac05dee8511b9af62\+                         6fe5b07bf271ce48df66cf4013d4b4b280bb74eac6206d772aa24f1e766bc813\+                         dc68a461736392ce97f8a04121028d75bdff57fcecff68f2927092b15074a452\+                         4a8d8182b6de761f6af3b8c6ba8f0000000002ce040000000000001976a914e7\+                         f58a9d4dfa6fdf481f03ff9e696c1c1eadd36688acb4168900000000001976a9\+                         14dd1eee80cebba451dc26f6423881b8836fe86a8388ac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Transaction not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transaction/{txid}/after/{height}':+    get:+      tags:+        - Transaction+      operationId: cbAfterHeight+      summary: Hard Fork Safety Check+      description: |+        Whether any ancestor of a transaction is a coinbase mined after a height.++        This answers a question that matters after a chain split, such as the one+        between BTC and BCH: coins created by miners after the split exist on one+        chain only, so a transaction that descends from them cannot be replayed on+        the other chain. The server follows the inputs of the transaction back+        through its ancestors, and answers `true` as soon as it finds such a+        coinbase, `false` when every path ends in older blocks, and `null` when it+        gives up after looking at 10,000 transactions, so that one request cannot+        keep the server busy for long.+      parameters:+        - $ref: '#/components/parameters/txidParam'+        - $ref: '#/components/parameters/blockHeightParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Specify height of last common block to find out if this transaction+            is invalid on alternate chain.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: boolean+                    nullable: true+                    example: true+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transactions':+    get:+      tags:+        - Transaction+      operationId: getTransactions+      summary: Multiple Detailed Transactions+      description: |+        Several transactions by id, in one call. Ids that are not known are left out+        of the response rather than failing the whole request, and repeated ids are+        answered once.+      parameters:+        - $ref: '#/components/parameters/txidsParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of detailed transaction objects.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Transaction'+              example:+                - txid: 02ea64dfff60b1f215282bbb4b2d729fecc50d81d2d0ef09a60d550a5a69418f+                  size: 217+                  version: 2+                  locktime: 0+                  fee: 218+                  inputs:+                    - coinbase: false+                      txid: a7319f9118f2dc743a17476e668b83f61e9ee5252cb0417baf0bea41ddd95ba6+                      output: 1+                      sigscript: "41f1489e83baad016eb63e3ebfe6bda264652fa188877e2efed9261566265cb3\+                                 1cff5091878f6a965b7184d538d8fe285b1ba4e3f18f53f44a30de3a36635750\+                                 c34121028d75bdff57fcecff68f2927092b15074a4524a8d8182b6de761f6af3\+                                 b8c6ba8f"+                      sequence: 0+                      pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                      value: 8987143+                      address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                      witness: []+                  outputs:+                    - address: bitcoincash:prgmpvfetym4fm724dddyrgnm5a7stjmlcp8v7z3tn+                      pkscript: a914d1b0b139593754efcaab5ad20d13dd3be82e5bfe87+                      value: 1231+                      spent: false+                      spender: null+                    - address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                      pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                      value: 8985694+                      spent: true+                      spender:+                        txid: 03c16602a41ae7cbc2a25b0d2801fdc3295e2e8ebfb26774030b2f7d538f5568+                        input: 0+                  block:+                    height: 688803+                    position: 17+                  deleted: false+                  time: 1621593002+                  rbf: false+                  weight: 868+                - txid: 03c16602a41ae7cbc2a25b0d2801fdc3295e2e8ebfb26774030b2f7d538f5568+                  size: 219+                  version: 2+                  locktime: 0+                  fee: 220+                  inputs:+                    - coinbase: false+                      txid: 02ea64dfff60b1f215282bbb4b2d729fecc50d81d2d0ef09a60d550a5a69418f+                      output: 1+                      sigscript: "41e3dc2001cffff05e62751c071ac05dee8511b9af626fe5b07bf271ce48df66\+                                 cf4013d4b4b280bb74eac6206d772aa24f1e766bc813dc68a461736392ce97f8\+                                 a04121028d75bdff57fcecff68f2927092b15074a4524a8d8182b6de761f6af3\+                                 b8c6ba8f"+                      sequence: 0+                      pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                      value: 8985694+                      address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                      witness: []+                  outputs:+                    - address: bitcoincash:qrnltz5afhaxlh6grupll8nfdswpatwnvcsyucwcle+                      pkscript: 76a914e7f58a9d4dfa6fdf481f03ff9e696c1c1eadd36688ac+                      value: 1230+                      spent: false+                      spender: null+                    - address: bitcoincash:qrw3am5qe6a6g5wuymmyywyphzpkl6r2svhy9jy2kz+                      pkscript: 76a914dd1eee80cebba451dc26f6423881b8836fe86a8388ac+                      value: 8984244+                      spent: true+                      spender:+                        txid: 4670c8b46211c06aa1a1c42e8c7e090e971c2549d30aac65cdb12876782c2248+                        input: 0+                  block:+                    height: 688804+                    position: 13+                  deleted: false+                  time: 1621593351+                  rbf: false+                  weight: 876+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++    post:+      tags:+        - Transaction+      summary: Broadcast Transaction+      description: |+        Send a signed transaction to the network, and get its id back.++        The body is the transaction in hexadecimal (white space is ignored) or as+        raw bytes. It must be in the format that nodes of the network read: on BCH,+        which has no SegWit, the format without witness data.++        **A 200 means the transaction was sent, not that the network accepted it.**+        The server does not validate transactions; it gives the transaction to half+        of its peers, which check it as they would any other. If they accept it, they+        relay it back, and it appears in the mempool and in balances within seconds.+        To know whether it was accepted, look for it with `/transaction/{txid}` or+        watch `/events`.++        Five seconds later the server asks those peers for the transaction back. A+        peer that accepted it sends it, and the server imports it like any other+        transaction it hears about; a peer that refused it has nothing to send.+      operationId: postTransaction+      parameters:+        - $ref: '#/components/parameters/prettyParam'+      requestBody:+        description: >-+          Publish Transaction. The body holds one transaction and nothing+          else. Bytes after the transaction are refused. The transaction is+          in the format that peers of the network read. A network without+          SegWit, such as Bitcoin Cash, takes the format without witness+          only.+        content:+          text/plain:+            schema:+              type: string+              description: Hex-encoded Bitcoin protocol transaction.+              example: "0200000001bd7d375bfc0a4ac4246ccefba4df1c635e5627b607bdc4af22282e\+                       cd5869d8f9010000006441f92d192c9e33b8184c58b88eb11f57a1eb5b5a2b0e\+                       72b326864021d9e946cd2a5275efcf385fbc0d9e7c8a76f9189d624efc00ff74\+                       657a52e47efc6ffa0bfe3b412102a20b2f9bd8b475252ad054fdedb49045bd40\+                       c8926b65f64e312dd821b243a37c000000000211180000000000001976a91434\+                       04e444809abd438d85eb18e8d07c796e364afe88ac399b7c00000000001976a9\+                       1486a7f1a2047ed6719fa1ad5484f15c37c104887c88ac00000000"+          application/octet-stream:+            schema:+              type: string+              format: binary+              description: Bitcoin protocol transaction.+      responses:+        '200':+          description: Transaction accepted by network peer.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/TxId'+              example:+                txid: 0b7cb07d848959fbd02ad6e2ed21fac888b8e8011c6b8c5362d05c703066ae67+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: Transaction rejected.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+        '500':+          description: |+            Publishing transaction failed for one of these reasons:++            * No network peers available.+            * Timeout while publishing, most likely silent rejection.+            * Network peer disconnected while publishing transaction.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transactions/block/{block}':+    get:+      tags:+        - Transaction+        - Block+      operationId: getBlockTransactions+      summary: Transactions for Block+      description: |+        The full transactions of a block, in the order of the block.+      parameters:+        - $ref: '#/components/parameters/blockParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Detailed transactions from specified Block.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Transaction'+              example:+                - txid: 8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87+                  size: 135+                  version: 1+                  locktime: 0+                  fee: 0+                  inputs:+                    - coinbase: true+                      txid: "0000000000000000000000000000000000000000000000000000000000000000"+                      output: 4294967295+                      sigscript: 044c86041b020602+                      sequence: 4294967295+                      pkscript: null+                      value: null+                      address: null+                      witness: []+                  outputs:+                    - address: null+                      pkscript: "41041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd74575\+                                16c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276f\+                                df84ac"+                      value: 5000000000+                      spent: true+                      spender:+                        txid: f3e6066078e815bb24db0dfbff814f738943bddaaa76f8beba360cfe2882480a+                        input: 12+                  block:+                    height: 100000+                    position: 0+                  deleted: false+                  time: 1293623863+                  rbf: false+                  weight: 540+                - txid: fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4+                  size: 259+                  version: 1+                  locktime: 0+                  fee: 0+                  inputs:+                    - coinbase: false+                      txid: 87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03+                      output: 0+                      sigscript: "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b\+                                 58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc\+                                 6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164\+                                 593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f8\+                                 7c161bc6f8a630121df2b3d3"+                      sequence: 4294967295+                      pkscript: 76a91471d7dd96d9edda09180fe9d57a477b5acc9cad1188ac+                      value: 5000000000+                      address: bitcoincash:qpca0hvkm8ka5zgcpl5a27j80ddve89dzy4ya2rc9q+                      witness: []+                  outputs:+                    - address: bitcoincash:qrpe3mafcwft5cqnchsyaeefw4000avtxgzc3zhzg2+                      pkscript: 76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac+                      value: 556000000+                      spent: true+                      spender:+                        txid: 5aa8e36f9423ee5fcf17c1d0d45d6988b8a5773eae8ad25d945bf34352040009+                        input: 6+                    - address: bitcoincash:qz2gcaj6dy2dg0e20tqh0k3v9a449h3a0smpvqhnkh+                      pkscript: 76a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac+                      value: 4444000000+                      spent: true+                      spender:+                        txid: 220ebc64e21abece964927322cba69180ed853bb187fbc6923bac7d010b9d87a+                        input: 0+                  block:+                    height: 100000+                    position: 1+                  deleted: false+                  time: 1293623863+                  rbf: false+                  weight: 1036+                - txid: 6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4+                  size: 257+                  version: 1+                  locktime: 0+                  fee: 0+                  inputs:+                    - coinbase: false+                      txid: cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3+                      output: 1+                      sigscript: "4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27942ae90a32\+                                 31e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe219a886b8426\+                                 9eaeb81e26b4fe014104ae31c31bf91278d99b8377a35bbce5b27d9fff154568\+                                 39e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b90da4631ee3\+                                 9560639db462e9cb850f"+                      sequence: 4294967295+                      pkscript: 76a91435fbee6a3bf8d99f17724ec54787567393a8a6b188ac+                      value: 300000000+                      address: bitcoincash:qq6lhmn280udn8chwf8v23u82eee829xkyfzh8lz80+                      witness: []+                  outputs:+                    - address: bitcoincash:qzcde0uha2l5gp8rrk2jga7wsgk6m0n7zq6q4n0ad0+                      pkscript: 76a914b0dcbf97eabf4404e31d952477ce822dadbe7e1088ac+                      value: 1000000+                      spent: true+                      spender:+                        txid: ae1933ba2b7dc9702c96b99fb00f1df1392d9dea998a24b88d61974e8da8d2ab+                        input: 0+                    - address: bitcoincash:qp439q0wcfdtfc0q0yllfcy2kx4mxsyumyaya6lvur+                      pkscript: 76a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac+                      value: 299000000+                      spent: true+                      spender:+                        txid: 20ed3fc9688a13fceeae5fdd606266c351b1f15b90404d9df387644e4c3d5b90+                        input: 3+                  block:+                    height: 100000+                    position: 2+                  deleted: false+                  time: 1293623863+                  rbf: false+                  weight: 1028+                - txid: e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d+                  size: 225+                  version: 1+                  locktime: 0+                  fee: 0+                  inputs:+                    - coinbase: false+                      txid: f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b+                      output: 0+                      sigscript: "493046022100bb1ad26df930a51cce110cf44f7a48c3c561fd977500b1ae5d6b\+                                 6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb465f3e6f8da12e\+                                 2c5303954aca7f78f3014104a7135bfe824c97ecc01ec7d7e336185c81e2aa2c\+                                 41ab175407c09484ce9694b44953fcb751206564a9c24dd094d42fdbfdd5aad3\+                                 e063ce6af4cfaaea4ea14fbb"+                      sequence: 4294967295+                      pkscript: 76a914c4eb47ecfdcf609a1848ee79acc2fa49d3caad7088ac+                      value: 1000000+                      address: bitcoincash:qrzwk3lvlh8kpxscfrh8ntxzlfya8j4dwqtfzxkdk7+                      witness: []+                  outputs:+                    - address: bitcoincash:qqu6502kncr2r4ujdhztuyvnexdl96u7uqczwuuqu6+                      pkscript: 76a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac+                      value: 1000000+                      spent: true+                      spender:+                        txid: 1ebf2dd645b815a59e35485dd4ceebc0587a1b7588b810e42085b479ce507bfa+                        input: 1+                  block:+                    height: 100000+                    position: 3+                  deleted: false+                  time: 1293623863+                  rbf: false+                  weight: 900+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transactions/block/{block}/raw':+    get:+      tags:+        - Transaction+        - Block+      operationId: getRawBlockTransactions+      summary: Raw Block Transactions+      description: |+        The transactions of a block, serialized as in the peer-to-peer protocol, in+        the order of the block.+      parameters:+        - $ref: '#/components/parameters/blockParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >-+            Bitcoin protocol transactions.+            Hex strings if using JSON.+          content:+            application/json:+              schema:+                type: array+                items:+                  type: string+              example:+                - "0100000001000000000000000000000000000000000000000000000000000000\+                  0000000000ffffffff08044c86041b020602ffffffff0100f2052a0100000043\+                  41041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd74575\+                  16c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276f\+                  df84ac00000000"+                - "0100000001032e38e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88\+                  fdf357a187000000008c493046022100c352d3dd993a981beba4a63ad15c2092\+                  75ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33e\+                  e7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d\+                  64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331\+                  586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3ffffffff0200e3232100\+                  0000001976a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe2\+                  08010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac00\+                  000000"+                - "0100000001c33ebff2a709f13d9f9a7569ab16a32786af7d7e2de09265e41c61\+                  d078294ecf010000008a4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8\+                  de6b342d27942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc\+                  3e628cbe219a886b84269eaeb81e26b4fe014104ae31c31bf91278d99b8377a3\+                  5bbce5b27d9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd34\+                  1c0fc3a7b90da4631ee39560639db462e9cb850fffffffff0240420f00000000\+                  001976a914b0dcbf97eabf4404e31d952477ce822dadbe7e1088acc060d21100\+                  0000001976a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac000000\+                  00"+                - "01000000010b6072b386d4a773235237f64c1126ac3b240c84b917a3909ba1c4\+                  3ded5f51f4000000008c493046022100bb1ad26df930a51cce110cf44f7a48c3\+                  c561fd977500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd\+                  574bdb465f3e6f8da12e2c5303954aca7f78f3014104a7135bfe824c97ecc01e\+                  c7d7e336185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c2\+                  4dd094d42fdbfdd5aad3e063ce6af4cfaaea4ea14fbbffffffff0140420f0000\+                  0000001976a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac000000\+                  00"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/transactions/raw':+    get:+      tags:+        - Transaction+      operationId: getRawTransactions+      summary: Raw Transactions+      description: |+        Several transactions by id, serialized as in the peer-to-peer protocol. Ids+        that are not known are left out.+      parameters:+        - $ref: '#/components/parameters/txidsParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: >+            Bitcoin protocol transactions.+            Hex strings if using JSON.+          content:+            application/json:+              schema:+                type: array+                items:+                  type: string+              example:+                - "02000000018f41695a0a550da609efd0d2810dc5ec9f722d4bbb2b2815f2b160\+                  ffdf64ea02010000006441e3dc2001cffff05e62751c071ac05dee8511b9af62\+                  6fe5b07bf271ce48df66cf4013d4b4b280bb74eac6206d772aa24f1e766bc813\+                  dc68a461736392ce97f8a04121028d75bdff57fcecff68f2927092b15074a452\+                  4a8d8182b6de761f6af3b8c6ba8f0000000002ce040000000000001976a914e7\+                  f58a9d4dfa6fdf481f03ff9e696c1c1eadd36688acb4168900000000001976a9\+                  14dd1eee80cebba451dc26f6423881b8836fe86a8388ac00000000"+                - "0200000001a65bd9dd41ea0baf7b41b02c25e59e1ef6838b666e47173a74dcf2\+                  18919f31a7010000006441f1489e83baad016eb63e3ebfe6bda264652fa18887\+                  7e2efed9261566265cb31cff5091878f6a965b7184d538d8fe285b1ba4e3f18f\+                  53f44a30de3a36635750c34121028d75bdff57fcecff68f2927092b15074a452\+                  4a8d8182b6de761f6af3b8c6ba8f0000000002cf0400000000000017a914d1b0\+                  b139593754efcaab5ad20d13dd3be82e5bfe875e1c8900000000001976a914dd\+                  1eee80cebba451dc26f6423881b8836fe86a8388ac00000000"+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/mempool':+    get:+      tags:+        - Transaction+      operationId: getMempool+      summary: Mempool Transaction Hashes+      description: |+        The ids of the unconfirmed transactions, newest first, by the time this+        server first saw them.++        There is no maximum `limit` here: the mempool is bounded by the network, and+        ids are small.+      parameters:+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of transaction hashes from mempool.+          content:+            application/json:+              schema:+                type: array+                items:+                  type: string+              example:+                - a60e3b0775e08877d0a8edac874090034f2fa55a15fb339b98b18abb5c8cbdf3+                - c9c50b1f75b4a69319c51ffae3a47df7de99b02089e696b711244fd4591eb51e+                - 11acbc09285f99d2cc11b707b61952258d554dcffe9a91ab0f6a22d1b06ac618+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/events':+    get:+      tags:+        - Block+        - Transaction+      operationId: getEventStream+      parameters:+        - $ref: '#/components/parameters/prettyParam'+        - name: after+          in: query+          required: false+          description: |+            Hash of the last event heard, a block or a transaction: the stream+            starts with what came after it.+          schema:+            type: string+          example: 3e225aa5d41b0f7560ee641f86b00728adc626102b348f9d319e0fdfd848a0db+        - name: quiet+          in: query+          required: false+          description: |+            `true` to hear of blocks alone, until something is watched.+          schema:+            type: boolean+            default: false+        - name: addresses+          in: query+          required: false+          description: Addresses to watch from the start, separated by commas.+          schema:+            type: string+        - name: xpub+          in: query+          required: false+          description: Extended public key to watch from the start.+          schema:+            type: string+        - name: derive+          in: query+          required: false+          description: |+            Types of the addresses of `xpub`: `legacy`, `compat` or `segwit`,+            one or several separated by commas. `legacy` when left out.+          schema:+            type: string+          example: legacy,segwit+        - name: filter+          in: query+          required: false+          description: |+            Bloom filter to start with, in hex, with `functions`, and `tweak`+            and `flags` when they are not 0.+          schema:+            type: string+        - name: functions+          in: query+          required: false+          description: Number of hash functions of `filter`.+          schema:+            type: integer+        - name: tweak+          in: query+          required: false+          description: Tweak of `filter`.+          schema:+            type: integer+        - name: flags+          in: query+          required: false+          description: Flags of `filter`.+          schema:+            type: integer+      summary: Stream Blocks and Transactions+      description: |+        A stream of what happens on the network: one event per new block and per+        transaction that enters or leaves the mempool, as one line of JSON each.++        ```+        {"type":"tx","id":"9a5c…"}+        {"type":"block","id":"0000000000000000000a1b…"}+        ```++        An event names what changed and nothing else, which keeps the stream+        small; fetch the block or transaction if you need it. A transaction+        that a block confirms has no event of its own: the block is the event.++        ## Taking up where you left++        The server keeps its last 10,000 events, fewer when their transactions+        are very large. With `after` the stream starts after the event with+        that hash, so a client that reconnects misses nothing. If the server+        does not have that event the answer is 404: read again what you+        follow, and connect without `after`. A client that falls further+        behind than the server keeps is disconnected.++        A transaction that entered the mempool and left it unconfirmed has two+        events with the same hash. The stream starts after the first of them,+        so an event may come twice, and none is left out.++        In a fleet every server keeps the same events, also from before it+        started: come back to any of them.++        ## WebSocket++        The same events are sent over a **WebSocket** at the same path, one+        message each, and `after` works the same. The server pings every 30+        seconds and drops clients that do not answer within 60.++        Messages are JSON in text frames or binary in binary frames ("Binary+        format" above). The server answers in the kind of frame that the+        client last sent; before the client has sent any, in binary if the+        `Accept` header of the handshake asked for it, else in JSON.++        ## Choosing transactions++        Over the WebSocket a client can say which transactions it wants to+        hear about, with these messages. Blocks are always sent. A client that+        sends none of them hears of every transaction.++        | Message | Does |+        | --- | --- |+        | `{"type":"watch","addresses":["…"]}` | Watch these addresses too. |+        | `{"type":"unwatch","addresses":["…"]}` | Stop watching them. |+        | `{"type":"watch","xpub":"…","derive":"segwit"}` | Watch a key too. |+        | `{"type":"unwatch","xpub":"…","derive":"segwit"}` | Stop watching it. |+        | `{"type":"filterload","filter":"hex","functions":11,"tweak":0,"flags":0}` | Set the Bloom filter. |+        | `{"type":"filteradd","data":"hex"}` | Add an element to the filter. |+        | `{"type":"filterclear"}` | Remove the filter. |+        | `{"type":"all"}` | Forget all of it: every transaction again. |+        | `{"type":"none"}` | Forget all of it: blocks alone. |++        A transaction is sent when it pays to or spends from a watched address+        or an address of a watched key, or when it matches the filter.++        ## Choosing from the start++        A client that connects and then sends its messages is sent every+        transaction until the first of them is read. To avoid that, say in the+        URL what to start with, on the WebSocket or on the HTTP stream:++        ```+        /events?quiet=true+        /events?addresses=bc1q…,1A1z…+        /events?xpub=xpub6C…&derive=legacy,segwit+        /events?filter=b50f8a…&functions=11+        ```++        `quiet=true` starts with blocks alone; send messages afterwards to+        watch more. The others watch what they name, and can be combined. A+        list of addresses too long for a URL goes in messages after+        `quiet=true`, or, for the HTTP stream, in the form of a `POST`.++        - **Keys.** `derive` is `legacy`, `compat` or `segwit`, as on the+          `/xpub` endpoints, and `legacy` when left out. To watch one key under+          several types of address, send a message for each. The server+          watches the addresses in use and the gap of unused ones after them,+          and moves the gap on as payments come. At most 100 keys.+        - **Addresses.** At most 100,000.+        - **Bloom filter.** The filter of BIP 37, matched as a node matches it+          for an SPV client: against the hash of the transaction, the data in+          its output scripts, the outputs it spends and the data in its input+          scripts. One filter to a connection. The server never changes it:+          `flags` is accepted, but the outputs of matching transactions are+          not added, so add what you want to follow with `filteradd`. An empty+          filter matches everything.+        - **No answers.** As between a node and an SPV client, nothing is sent+          back. A message that the server cannot read or will not do closes+          the connection with code 1008 and the reason.+        - When the server does not know the transaction of an event, which+          happens when one leaves the mempool long after it entered, the event+          goes to every client.+      responses:+        '200':+          description: Events stream each on one line.+          content:+            application/x-json-stream:+              schema:+                $ref: '#/components/schemas/Event'+              example:+                type: tx+                id: 3e225aa5d41b0f7560ee641f86b00728adc626102b348f9d319e0fdfd848a0db+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: A parameter could not be read, or names too much to watch.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+        '404':+          description: The server has no event with the hash given in `after`.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/peers':+    get:+      tags:+        - Network+      operationId: getPeers+      parameters:+        - $ref: '#/components/parameters/prettyParam'+      summary: Connected Network Peers+      description: |+        The nodes of the peer-to-peer network that this server is connected to. It+        is where blocks and transactions come from, and where broadcast transactions+        go.+      responses:+        '200':+          description: List of peer objects.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Peer'+              example:+                - useragent: /BCH Unlimited:1.9.2(EB32; AD12; 64bit)/+                  address: 162.250.189.42:8333+                  version: 80003+                  services: "0000000000000875"+                  relay: true+                - useragent: /BCH Unlimited:1.9.2(EB32; AD12; 64bit)/+                  address: 154.57.12.149:8331+                  version: 80003+                  services: "0000000000000875"+                  relay: true+                - useragent: /Bitcoin Cash Node:23.0.0(EB32.0)/+                  address: 206.189.127.50:8333+                  version: 70015+                  services: "0000000000000425"+                  relay: true+            application/octet-stream:+              schema:+                type: string+                format: binary++  '/xpub/{xpub}':+    get:+      tags:+        - Address+      operationId: getXPubSummary+      summary: Extended Public Key Summary+      description: |+        The summary of a wallet: its balance, and the index of the last used address+        of the receiving and of the change chain, so the wallet knows where to derive+        its next address.++        It is the cheapest way to check a wallet: it does not list transactions or+        outputs. See *Extended public keys* at the top for how the addresses are+        found.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/nocacheParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Information about extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/XPubSummary'+              example:+                balance:+                  confirmed: 84994+                  unconfirmed: 0+                  received: 137003577+                  utxo: 3+                indices:+                  change: 27+                  external: 73+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+    delete:+      tags:+        - Address+      operationId: deleteCachedXPub+      summary: Delete Cached Extended Key+      description: |+        Remove a wallet from the Redis cache of this server, if it has one. The+        answer says whether it was cached.++        There is rarely a reason to call this: cached wallets are kept up to date+        with every block and transaction, and the least recently used ones are+        evicted when the cache is full. It is there to free memory, or to rule out+        the cache when looking into a problem.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: True if xpub got deleted from cache.+          content:+            application/json:+              schema:+                type: object+                required:+                  - result+                properties:+                  result:+                    type: boolean+                    nullable: false+                    example: true+            application/octet-stream:+              schema:+                type: string+                format: binary+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/xpub/{xpub}/transactions/full':+    get:+      tags:+        - Address+      operationId: getXPubTransactionsFull+      summary: Detailed Extended Public Key Transactions+      description: |+        Like `/xpub/{xpub}/transactions`, with the full transactions instead of+        their ids, and the lower maximum of full transactions.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/nocacheParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of transaction objects.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/Transaction'+              example:+                - txid: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                  size: 406+                  version: 1+                  locktime: 0+                  fee: 675+                  inputs:+                    - coinbase: false+                      txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                      output: 1+                      sigscript: "473044022073b48b298df7ffb4ed5bb54ae939968e82fa966bc92d61728c8f7e\+                                 6e6d914e1b022042164959fdbbd3cda2cdaf541a2143be79f4be85144bfef790\+                                 6223c8a8cb2b2a4121029c71d9b4b94e8aa48072d1affba3432560d3e7964849\+                                 1d4b6351d3ddff51aa8f"+                      sequence: 4294967295+                      pkscript: 76a91474e607acf5ecdef5f9746ffa280744413a595cdb88ac+                      value: 70000+                      address: bitcoincash:qp6wvpav7hkdaa0ew3hl52q8g3qn5k2umvmh76m9eh+                      witness: []+                    - coinbase: false+                      txid: 6fc0394cea4f5f82fe8f332118a9079469336be53bc73975e9d58a99fbbc7ca2+                      output: 1+                      sigscript: "47304402204dc38ccbb6b3f6dcf496b7405bfc3f1942c3834197a32af9e07e6a\+                                 1f2d916c9502203a9f536951cb672b76111ec58cca35971c06af2bfaa481bd1a\+                                 ee67a03428e78341210250dce9058b388bbc4014d3600770b9bb59b7636a2eef\+                                 591797d9572014065c94"+                      sequence: 4294967295+                      pkscript: 76a91473cdab4b3e2486f7cbe618845f106109e5920aa588ac+                      value: 546+                      address: bitcoincash:qpeum26t8cjgda7tucvgghcsvyy7tys255ej7ux8wg+                      witness: []+                  outputs:+                    - address: bitcoincash:qz3k0wyd62g3njyucgcnpd72zq0r7rpsmumj82gvl4+                      pkscript: 76a914a367b88dd29119c89cc23130b7ca101e3f0c30df88ac+                      value: 30000+                      spent: false+                      spender: null+                    - address: bitcoincash:qpfvtmtyvfh3m5ul9kyc8gxefdpk2fx0syxxf7f6py+                      pkscript: 76a91452c5ed64626f1dd39f2d8983a0d94b436524cf8188ac+                      value: 39325+                      spent: false+                      spender: null+                    - address: bitcoincash:qrdf6e456fx6cmp6nwn9vhp4t6es7cuv9cfhvgdg5m+                      pkscript: 76a914da9d66b4d24dac6c3a9ba6565c355eb30f638c2e88ac+                      value: 546+                      spent: true+                      spender:+                        txid: 8c9f0ab27f96baca66d7a2eee5db8acd6b0b5fc725afbc3c2ea69c3d5b04aa7d+                        input: 1+                  block:+                    height: 680268+                    position: 6664+                  deleted: false+                  time: 1616669866+                  rbf: false+                  weight: 1624+                - txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                  size: 851+                  version: 1+                  locktime: 0+                  fee: 1998+                  inputs:+                    - coinbase: false+                      txid: 26ce9896f0f64349670735bdc30fb42adadac61348e403e78c5f92c3fb697355+                      output: 0+                      sigscript: "483045022100b8feba7bfc7a0809c616694237bd5e82f6f3a5fafbe0bad03668\+                                 79732fa829bc02203dce83fe508b86a1449c404756c274e11ea47decc4f8802c\+                                 fd2ab7e92ed7b51b41210388e0b3a8e3aa50414ac8c03ae02ac1c9931d901ad7\+                                 b6c1b7b83d50554fb592d8"+                      sequence: 4294967295+                      pkscript: 76a914c58a0c635b60d94498c416d32d4a2c15b36f8a2388ac+                      value: 40537+                      address: bitcoincash:qrzc5rrrtdsdj3yccstdxt229s2mxmu2yvj7ppp3cl+                      witness: []+                    - coinbase: false+                      txid: 41a422e6c003596954f07eb5021d9a2d6951da297dd519a5de3885772ac60e7c+                      output: 1+                      sigscript: "483045022100ad61cb3b0f08af0ada0ebb5be16213377fa193c9da16e4dca58a\+                                 9350724106330220089ac5eea5193f0926a483668eef219c7add5fa7d2be99a6\+                                 3e41da985fc104a241210223dc345218b6feb1815f0f103bc30a8508e3e248c4\+                                 f9b4bfdfed1d4498f879b4"+                      sequence: 4294967295+                      pkscript: 76a9145590a6b61c7d22a199692ce9c55cef294acebbeb88ac+                      value: 40000+                      address: bitcoincash:qp2epf4kr37j9gvedykwn32uau554n4mavzhpyg2ua+                      witness: []+                    - coinbase: false+                      txid: de01f52644b2610d491d28ce474382c57655f102974526a3fba6b5a48f8ea812+                      output: 0+                      sigscript: "483045022100d29b0dd38462a33eac65eb8df53ba2b2905bac923057f5b37e20\+                                 94f1a6072355022017480d0f2eedaa5f3ff2ca620a7b5b9301292524dfcaaa9c\+                                 97859ee074f92b6e412102db25e4df201e02e95f167f63e06c55febe81fd0713\+                                 3fb09f65b07a356aa645ec"+                      sequence: 4294967295+                      pkscript: 76a91457725700f16eda53d75608972ff322dd05642a8f88ac+                      value: 3000+                      address: bitcoincash:qpthy4cq79hd557h2cyfwtlnytws2ep23u6k07qh8v+                      witness: []+                    - coinbase: false+                      txid: de01f52644b2610d491d28ce474382c57655f102974526a3fba6b5a48f8ea812+                      output: 1+                      sigscript: "4830450221008ee0107bfc71e063ff8d5ab98f83bb63f17bda387d049b2c9e7f\+                                 f885941a3859022015baabbc9280c70a19a6047f1028f261f5686b8342273316\+                                 b4e2d703ab72d6264121027b884db5a796d7f96412a2efbf5961ef824323c8b0\+                                 64e375c3e0dd512c6a010a"+                      sequence: 4294967295+                      pkscript: 76a9145f8e761ff252d3349c32e6bc12a5e11ba20aa3f188ac+                      value: 4130+                      address: bitcoincash:qp0cuasl7ffdxdyuxtntcy49uyd6yz4r7y8u0ss3yd+                      witness: []+                    - coinbase: false+                      txid: b0fddc9d29b86c695af3dd66b1834256c362698532585a597de9c800f04fde06+                      output: 401+                      sigscript: "473044022034251190cae8ba9197f81756778d5fbf071e63bb7460386fbb36cd\+                                 74d47560a9022050a1cfec6076b8e7f23cd6e843b64cb2582476fd4b44fadb38\+                                 7803ed9ad4f1e54121037779735684011f62563197944da9f9e074b17813c0c6\+                                 2e79aeac7d875f9b8bf2"+                      sequence: 4294967295+                      pkscript: 76a91475dcf340cbb686b30bdaf6a2c8f8c68dadf93a5a88ac+                      value: 546+                      address: bitcoincash:qp6aeu6qewmgdvctmtm29j8cc6x6m7f6tgc6ey2mqu+                      witness: []+                  outputs:+                    - address: bitcoincash:qpg4r52nu85ra68aj4uecg0mufzu9hkmj5pxlaxfp2+                      pkscript: 76a9145151d153e1e83ee8fd95799c21fbe245c2dedb9588ac+                      value: 15669+                      spent: false+                      spender: null+                    - address: bitcoincash:qp6wvpav7hkdaa0ew3hl52q8g3qn5k2umvmh76m9eh+                      pkscript: 76a91474e607acf5ecdef5f9746ffa280744413a595cdb88ac+                      value: 70000+                      spent: true+                      spender:+                        txid: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                        input: 0+                    - address: bitcoincash:qq0kufslm9n5v0t5m5vy9gtnuupvpep0wv57sfl7gj+                      pkscript: 76a9141f6e261fd967463d74dd1842a173e702c0e42f7388ac+                      value: 546+                      spent: true+                      spender:+                        txid: d84d05a569d944a92bcb9aa63049b9279369cd44e41ef365da13312ce680530f+                        input: 1+                  block:+                    height: 678972+                    position: 832+                  deleted: false+                  time: 1615894044+                  rbf: false+                  weight: 3404+                - txid: de01f52644b2610d491d28ce474382c57655f102974526a3fba6b5a48f8ea812+                  size: 406+                  version: 1+                  locktime: 0+                  fee: 675+                  inputs:+                    - coinbase: false+                      txid: 41a422e6c003596954f07eb5021d9a2d6951da297dd519a5de3885772ac60e7c+                      output: 0+                      sigscript: "473044022029a10619e7e98a3e7cb17821ab0725ef25b700a9b943965c8762ee\+                                 3b4e19546202200be34da0f640cd3209e5f11a3b1824409df92cae6a0efd8449\+                                 d8e22faaae7865412103b3a86c342fd06c33e94dcbe2565fc3d9d6c4c081caa0\+                                 f1ec42bb45bbbbaf2e43"+                      sequence: 4294967295+                      pkscript: 76a9144abb2deefe6ca46c695132adafd70f72d50cab2688ac+                      value: 7805+                      address: bitcoincash:qp9tkt0wlek2gmrf2ye2mt7hpaed2r9tycna42c5ax+                      witness: []+                    - coinbase: false+                      txid: 9a396103f3656b4e119f166610d7e41e24c01a5c45200a8fcb58d3b79fb0dcaf+                      output: 315+                      sigscript: "4730440220271872d09da2cc13b764b5bf13cc373da53d89421e0d31ac33a734\+                                 20c5e6a03902206e01bf837db72fa4340428e3d061685cc49ec156a65e4b48ba\+                                 bcfd5d6c89018f4121033da5e50b4615d790b9b755cf5db213b0d08d489587d8\+                                 946a7dfd1b8dc76a1829"+                      sequence: 4294967295+                      pkscript: 76a914485103c1b046fe0f3ca0ed9ced338b53437c0c8288ac+                      value: 546+                      address: bitcoincash:qpy9zq7pkpr0ureu5rkeemfn3df5xlqvsgf66pqne5+                      witness: []+                  outputs:+                    - address: bitcoincash:qpthy4cq79hd557h2cyfwtlnytws2ep23u6k07qh8v+                      pkscript: 76a91457725700f16eda53d75608972ff322dd05642a8f88ac+                      value: 3000+                      spent: true+                      spender:+                        txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                        input: 2+                    - address: bitcoincash:qp0cuasl7ffdxdyuxtntcy49uyd6yz4r7y8u0ss3yd+                      pkscript: 76a9145f8e761ff252d3349c32e6bc12a5e11ba20aa3f188ac+                      value: 4130+                      spent: true+                      spender:+                        txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                        input: 3+                    - address: bitcoincash:qrcdtjvwn9v2r94y6flka67tvqt99a9cjq3fk9k8ws+                      pkscript: 76a914f0d5c98e9958a196a4d27f6eebcb601652f4b89088ac+                      value: 546+                      spent: true+                      spender:+                        txid: a93609dbae718b5f6c9ab69cf3f8d979636f09a411f057db8fa65b17556de601+                        input: 1+                  block:+                    height: 678151+                    position: 657+                  deleted: false+                  time: 1615384806+                  rbf: false+                  weight: 1624++            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/xpub/{xpub}/transactions':+    get:+      tags:+        - Address+      operationId: getXPubTransactions+      summary: Extended Public Key Transaction List+      description: |+        The transactions of every address of a wallet, merged into one list, newest+        first, as transaction ids with their place in the chain. A transaction+        between two addresses of the wallet appears once.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/nocacheParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of transactions.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/TxRef'+              example:+                - txid: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                  block:+                    height: 680268+                    position: 6664+                - txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                  block:+                    height: 678972+                    position: 832+                - txid: de01f52644b2610d491d28ce474382c57655f102974526a3fba6b5a48f8ea812+                  block:+                    height: 678151+                    position: 657+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/xpub/{xpub}/balances':+    get:+      tags:+        - Address+      operationId: getXPubBalances+      summary: Extended Public Key Balances+      description: |+        The balance of every address of a wallet that the server looked at, with its+        derivation path: each used address, and the unused ones of the gap, with+        zeros, so a wallet can see which addresses it may still hand out. For the+        total, use `/xpub/{xpub}`.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/nocacheParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: Balances only for used addresses.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/XPubBalance'+              example:+                - path:+                    - 0+                    - 0+                  balance:+                    address: bitcoincash:qpyq63tvlk0pca8mah6a8c0zxrvq8qvjzymrrnwprn+                    confirmed: 0+                    unconfirmed: 0+                    utxo: 0+                    txs: 8+                    received: 4530000+                - path:+                    - 0+                    - 1+                  balance:+                    address: bitcoincash:qqjfkpxln3ya6dq7l27ffdxcmq2h9hlfjvr70vk9l7+                    confirmed: 0+                    unconfirmed: 0+                    utxo: 0+                    txs: 2+                    received: 10000+                - path:+                    - 0+                    - 2+                  balance:+                    address: bitcoincash:qzxfymuk5t4veu0kxf7t0cmkn76t69skuggxaq2q8d+                    confirmed: 0+                    unconfirmed: 0+                    utxo: 0+                    txs: 2+                    received: 23000+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/xpub/{xpub}/unspent':+    get:+      tags:+        - Address+      operationId: getXPubUnspent+      summary: Extended Public Key Unspent Outputs+      description: |+        The unspent outputs of a wallet, newest first, each with the derivation path+        of its address, so the wallet knows which key signs for it.+      parameters:+        - $ref: '#/components/parameters/xpubParam'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/heightParam'+        - $ref: '#/components/parameters/limitParam'+        - $ref: '#/components/parameters/offsetParam'+        - $ref: '#/components/parameters/nocacheParam'+        - $ref: '#/components/parameters/prettyParam'+      responses:+        '200':+          description: List of unspent output objects.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/XPubUnspent'+              example:+                - unspent:+                    address: bitcoincash:qz3k0wyd62g3njyucgcnpd72zq0r7rpsmumj82gvl4+                    block:+                      height: 680268+                      position: 6664+                    txid: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    index: 0+                    pkscript: 76a914a367b88dd29119c89cc23130b7ca101e3f0c30df88ac+                    value: 30000+                  path:+                    - 0+                    - 73+                - unspent:+                    address: bitcoincash:qpfvtmtyvfh3m5ul9kyc8gxefdpk2fx0syxxf7f6py+                    block:+                      height: 680268+                      position: 6664+                    txid: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    index: 1+                    pkscript: 76a91452c5ed64626f1dd39f2d8983a0d94b436524cf8188ac+                    value: 39325+                  path:+                    - 1+                    - 27+                - unspent:+                    address: bitcoincash:qpg4r52nu85ra68aj4uecg0mufzu9hkmj5pxlaxfp2+                    block:+                      height: 678972+                      position: 832+                    txid: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                    index: 0+                    pkscript: 76a9145151d153e1e83ee8fd95799c21fbe245c2dedb9588ac+                    value: 15669+                  path:+                    - 1+                    - 26+            application/octet-stream:+              schema:+                type: string+                format: binary+        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid extended public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/balance':+    get:+      tags:+        - Blockchain+      operationId: getBinfoBalance+      summary: Address or XPub Balances+      description: |+        Short balances of addresses and wallets, keyed by the address or xpub as it+        was given, as Blockchain.info answers.+      parameters:+        - $ref: '#/components/parameters/binfoActive'+        - $ref: '#/components/parameters/binfoActiveP2SH'+        - $ref: '#/components/parameters/binfoActiveBech32'+        - $ref: '#/components/parameters/binfoCashAddr'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Balance object map.+          content:+            application/json:+              schema:+                type: object+                additionalProperties:+                  $ref: '#/components/schemas/BinfoShortBalance'+              example:+                ? "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                  ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                :+                  final_balance: 84994+                  n_tx: 123+                  total_received: 137003577+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+    post:+      tags:+        - Blockchain+      operationId: postBinfoBalance+      summary: Address or XPub Balances+      description: |+        Like the `GET`, with the parameters in a form in the body, for requests with+        too many addresses to fit in a URL.+      requestBody:+        content:+          application/x-www-form-urlencoded:+            schema:+              $ref: '#/components/schemas/BinfoBalanceReq'+      responses:+        '200':+          description: Balance object map.+          content:+            application/json:+              schema:+                type: object+                additionalProperties:+                  $ref: '#/components/schemas/BinfoShortBalance'+              example:+                ? "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                  ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                :+                  final_balance: 84994+                  n_tx: 123+                  total_received: 137003577+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/rawaddr/{address}':+    get:+      tags:+        - Blockchain+      operationId: getBinfoRawAddr+      summary: Address Balance & Transactions+      description: |+        The balance and transactions of an address or an xpub, as Blockchain.info+        answers. Also served at `/blockchain/address/{address}` and+        `/blockchain/xpub/{address}`, for clients that use those paths.+      parameters:+        - $ref: '#/components/parameters/binfoAddr'+        - $ref: '#/components/parameters/binfoLimit'+        - $ref: '#/components/parameters/binfoOffset'+        - $ref: '#/components/parameters/xpubDerive'+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Address balance & transactions.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoRawAddr'+              example:+                hash160: 37d070b3c6716a6174080722579fbdddad39bfb3+                address: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                n_tx: 2+                n_unredeemed: 0+                total_received: 2275183814+                total_sent: 2275183814+                final_balance: 0+                txs:+                  - hash: 00f304a6089a708168cd10d5ad8abd2977c36791156a55b040117cb5a3db7fad+                    ver: 2+                    vin_sz: 1+                    vout_sz: 2+                    size: 225+                    weight: 900+                    fee: 4500+                    relayed_by: 0.0.0.0+                    lock_time: 688792+                    tx_index: 6104469036511106+                    double_spend: false+                    time: 1621583361+                    block_index: 688793+                    block_height: 688793+                    inputs:+                      - sequence: 4294967294+                        witness: ""+                        script: "47304402206a11f253c8035be2d9f5958a9ca838b3fd5a1a120441885e10e6ca\+                                d0fef3911f02202056263939a88fe06e7fc94368ff44c46f71e7daa603936d75\+                                5b35b8b2a21c154121027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d\+                                4c07b9f8c3d04327794c"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                          spending_outpoints:+                            - tx_index: 6104469036511106+                              n: 0+                          tx_index: 5028695321485056+                          value: 2275183814+                          addr: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                          n: 1+                          type: 0+                    out:+                      - type: 0+                        spent: false+                        value: 15197686+                        spending_outpoints: []+                        n: 0+                        tx_index: 6104469036511106+                        script: 76a9143fd3cf57704a93e9782a97a4fb5a3bf7fb1a604788ac+                        addr: bitcoincash:qqla8n6hwp9f86tc92t6f76680mlkxnqgu5flcqwfe+                      - type: 0+                        spent: true+                        value: 2259981628+                        spending_outpoints:+                          - tx_index: 8410203892883835+                            n: 0+                        n: 1+                        tx_index: 6104469036511106+                        script: 76a9143ea707954f63a225fe4630701df0924c9e835a3488ac+                        addr: bitcoincash:qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2+                    result: -2275183814+                    balance: 0+                  - hash: b810589c80335083ed715c03abf62f23968846d2f54343bac305f890f292ec8e+                    ver: 2+                    vin_sz: 1+                    vout_sz: 2+                    size: 225+                    weight: 900+                    fee: 4500+                    relayed_by: 0.0.0.0+                    lock_time: 688706+                    tx_index: 5028695321485056+                    double_spend: false+                    time: 1621533785+                    block_index: 688707+                    block_height: 688707+                    inputs:+                      - sequence: 4294967294+                        witness: ""+                        script: "4730440220298e397d155bcd0c3371a17f11d6dc06392c72c6d4646ac27fad72\+                                e86e7f18f50220471c7338a0f4573955e52a95c4c2717f5ae5d369ad153ae627\+                                49d0dcd9250482412103c60a0fd16eaf3c72dee4bf12a566ee3a6ae181a29b66\+                                a1f2407442f7a6d39ad2"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a9145124c4e68eed7ffc6110c2326d7d3739f55a09eb88ac+                          spending_outpoints:+                            - tx_index: 5028695321485056+                              n: 0+                          tx_index: 8915933840879415+                          value: 2775188314+                          addr: bitcoincash:qpgjf38x3mkhllrpzrprymtaxuul2ksfavle5hmhl4+                          n: 0+                          type: 0+                    out:+                      - type: 0+                        spent: false+                        value: 500000000+                        spending_outpoints: []+                        n: 0+                        tx_index: 5028695321485056+                        script: 76a9140cfccdb98e0de78db0deffae8681c60b306e70a088ac+                        addr: bitcoincash:qqx0ende3cx70rdsmml6ap5pcc9nqmns5qlc8yvhga+                      - type: 0+                        spent: true+                        value: 2275183814+                        spending_outpoints:+                          - tx_index: 6104469036511106+                            n: 0+                        n: 1+                        tx_index: 5028695321485056+                        script: 76a91437d070b3c6716a6174080722579fbdddad39bfb388ac+                        addr: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+                    result: 2275183814+                    balance: 2275183814++        '400':+          description: |+            The wallet has no transactions with this `derive`, but has some with+            another kind of address; the message names it. A query parameter+            that does not parse also gives 400.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: user-error+                message: >-+                  This xpub has no transactions with derive=legacy, but has+                  some with derive=segwit; ask with derive=segwit.+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/unconfirmed-transactions':+    get:+      tags:+        - Blockchain+      operationId: getBinfoMempool+      summary: Mempool Transactions+      description: |+        Unconfirmed transactions, newest first, as Blockchain.info answers.+      parameters:+        - $ref: '#/components/parameters/binfoLimit'+        - $ref: '#/components/parameters/binfoOffset'+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Mempool transactions array.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BinfoTx'+              example:+                txs:+                  - hash: 2e77a3d4991383bd9b5fa0e286ba00e5abcf86414edb87581a559957dc674bfa+                    ver: 2+                    vin_sz: 1+                    vout_sz: 2+                    size: 225+                    weight: 900+                    fee: 10000+                    relayed_by: 0.0.0.0+                    lock_time: 0+                    tx_index: 8806456703513386+                    double_spend: false+                    time: 1621613080+                    block_index: null+                    block_height: null+                    inputs:+                      - sequence: 4294967295+                        witness: ""+                        script: "473044022027aaf94096c0b4c3e0a8405078db67f3c8680b0e1def70c5dac307\+                                cd0f4f6c4b022079ce8b9dbf26f9114a9a16575b478214294274d4158672ffaa\+                                758c8904aff91b4121031b3c18ce8efcf9a1ce214cb5c62489e842663b47908b\+                                43f3af789d840af71efb"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a914842b152a0bbd4647afaeceec8a6afaa90668e7c788ac+                          spending_outpoints:+                            - tx_index: 8806456703513386+                              n: 0+                          tx_index: 4182415564665005+                          value: 267599400+                          addr: bitcoincash:qzzzk9f2pw75v3a04m8wezn2l25sv688cua892qu7q+                          n: 0+                          type: 0+                    out:+                      - type: 0+                        spent: false+                        value: 1002000+                        spending_outpoints: []+                        n: 0+                        tx_index: 8806456703513386+                        script: 76a914e2102caa6155620b64f363295adc9c21074a26bb88ac+                        addr: bitcoincash:qr3pqt92v92kyzmy7d3jjkkunssswj3xhv4zmfpu9p+                      - type: 0+                        spent: false+                        value: 266587400+                        spending_outpoints: []+                        n: 1+                        tx_index: 8806456703513386+                        script: 76a914842b152a0bbd4647afaeceec8a6afaa90668e7c788ac+                        addr: bitcoincash:qzzzk9f2pw75v3a04m8wezn2l25sv688cua892qu7q+                  - hash: 12a015002de59ce4f6d3510df37e56ebde864138acdd35051618603173ca6ee5+                    ver: 2+                    vin_sz: 1+                    vout_sz: 2+                    size: 219+                    weight: 876+                    fee: 220+                    relayed_by: 0.0.0.0+                    lock_time: 0+                    tx_index: 8072448182725635+                    double_spend: false+                    time: 1621613078+                    block_index: null+                    block_height: null+                    inputs:+                      - sequence: 0+                        witness: ""+                        script: "41c49770d0b68eb9d4552a45d3ce23ab7dc84ecded730f76903b9a4ff948ace2\+                                92ba282b63ea1fa3a4203f79edffc8d00b7ef68be4a42bc530be9e9c9a6a0d2c\+                                d4412102ed4717b0f0ae1fd42423d8fa2c9ae36783bc53f7acd8839f68dc5ad5\+                                8a9cf8b2"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a9145638dda8466c4a9c7fc8557d504272b7403f4dc288ac+                          spending_outpoints:+                            - tx_index: 8072448182725635+                              n: 0+                          tx_index: 1836562911653034+                          value: 7514960+                          addr: bitcoincash:qptr3hdggeky48rlep2h65zzw2m5q06dcgnpyymlh5+                          n: 1+                          type: 0+                    out:+                      - type: 0+                        spent: false+                        value: 733+                        spending_outpoints: []+                        n: 0+                        tx_index: 8072448182725635+                        script: 76a9145d0bafa91a9d13e1958a8c2c57de70047abd5f6688ac+                        addr: bitcoincash:qpwshtafr2w38cv432xzc477wqz8402lvczv7zvppp+                      - type: 0+                        spent: false+                        value: 7514007+                        spending_outpoints: []+                        n: 1+                        tx_index: 8072448182725635+                        script: 76a9145638dda8466c4a9c7fc8557d504272b7403f4dc288ac+                        addr: bitcoincash:qptr3hdggeky48rlep2h65zzw2m5q06dcgnpyymlh5+++  '/blockchain/multiaddr':+    get:+      tags:+        - Blockchain+      operationId: getBinfoMultiAddr+      summary: Wallet Data+      description: |+        The balances and transactions of a wallet made of addresses and xpubs, as+        Blockchain.info answers, with the current price of the coin in a currency+        of your choice.++        Transactions show the change to the balance of the wallet as a whole, and,+        by default, incoming transactions leave out outputs to other addresses+        (`noCompact` keeps them): a payment from an exchange can have hundreds of+        outputs to other people, which the wallet has no use for.+      parameters:+        - $ref: '#/components/parameters/binfoActive'+        - $ref: '#/components/parameters/binfoActiveP2SH'+        - $ref: '#/components/parameters/binfoActiveBech32'+        - $ref: '#/components/parameters/binfoOnlyShow'+        - $ref: '#/components/parameters/binfoCount'+        - $ref: '#/components/parameters/binfoOffset'+        - $ref: '#/components/parameters/binfoNoCompact'+        - $ref: '#/components/parameters/binfoCurrency'+        - $ref: '#/components/parameters/binfoCashAddr'+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoFilter'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Wallet information object.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoMultiAddr'+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+    post:+      tags:+        - Blockchain+      operationId: postBinfoMultiAddr+      summary: Wallet Data+      description: |+        Like the `GET`, with the parameters in a form in the body, for wallets with+        too many addresses to fit in a URL.+      requestBody:+        content:+          application/x-www-form-urlencoded:+            schema:+              $ref: '#/components/schemas/BinfoMultiAddrReq'+      responses:+        '200':+          description: Wallet information object.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoMultiAddr'+              example:+                addresses:+                  - address: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                             ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                    change_index: 28+                    account_index: 74+                    final_balance: 84994+                    n_tx: 123+                    total_received: 137003577+                    total_sent: 136918583+                wallet:+                  final_balance: 84994+                  n_tx: 2+                  n_tx_filtered: 2+                  total_received: 137003577+                  total_sent: 136918583+                txs:+                  - hash: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                    ver: 1+                    vin_sz: 2+                    vout_sz: 3+                    size: 406+                    weight: 1624+                    fee: 675+                    relayed_by: 0.0.0.0+                    lock_time: 0+                    tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                    double_spend: false+                    time: 1613556477+                    block_index: 675214+                    block_height: 675214+                    inputs:+                      - sequence: 4294967295+                        witness: ""+                        script: "47304402200e85d6f05cf9675f60382fda3e245695e12bb478128c701447e8b8\+                                5d0321921d02200a8624652009ce2413718d748194ab020aef2945721c9a29e8\+                                59de034a00291841210266490eabd32e0a0eb4de6089590e6dbf5bd8b5411a3e\+                                c4b78cf40091e7200c23"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a91437be7ac52f6f08899051efe89f228c14a171bb2588ac+                          xpub:+                            path: M/0/66+                            m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                               ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                          spending_outpoints:+                            - tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                              n: 0+                          tx_index: 671ba5996381a919c7b88415513ca1db54528e3741282a44228f7eea4d23ffad+                          value: 21000+                          addr: bitcoincash:qqmmu7k99ahs3zvs28h738ez3s22zudmy5squrulad+                          n: 1+                          type: 0+                      - sequence: 4294967295+                        witness: ""+                        script: "473044022030ade2f8405e47001dfe816fb2047cb4f6beff185c942e325d7b30\+                                70dca66622022001c0bb027e88d3ea2fce18c47553461dd063a0a26aa0f69252\+                                dcce8e26fe8cbc41210269115d4edff9823c494195a0e3a9497d868c61c43c76\+                                c37f851e28655d4fad60"+                        index: 1+                        prev_out:+                          spent: true+                          script: 76a914f1d4f42b339a3bc881085e4babfb0db98db8887888ac+                          spending_outpoints:+                            - tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                              n: 1+                          tx_index: ac99b50918e84857b84a13b27a0635a9f0474d6e20ddae0b5d05f3b8503d3e5c+                          value: 546+                          addr: bitcoincash:qrcafaptxwdrhjyppp0yh2lmpkucmwyg0qf590pgku+                          n: 1+                          type: 0+                    out:+                      - type: 0+                        spent: true+                        value: 4485+                        spending_outpoints:+                          - tx_index: 26ce9896f0f64349670735bdc30fb42adadac61348e403e78c5f92c3fb697355+                            n: 2+                        n: 0+                        tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                        script: 76a9147cf27910c2aa3e86e9d69f5bb8dc0f677a0e83fb88ac+                        addr: bitcoincash:qp70y7gsc24raphf6604hwxupanh5r5rlvh3jvjjan+                        xpub:+                          path: M/0/68+                          m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                             ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      - type: 0+                        spent: true+                        value: 15840+                        spending_outpoints:+                          - tx_index: 26ce9896f0f64349670735bdc30fb42adadac61348e403e78c5f92c3fb697355+                            n: 3+                        n: 1+                        tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                        script: 76a91457743547f518ef1ebf928e0065036230f246680288ac+                        addr: bitcoincash:qpthgd2875vw784lj28qqegrvgc0y3ngqga8fpcy8k+                        xpub:+                          path: M/1/22+                          m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                             ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      - type: 0+                        spent: false+                        value: 546+                        spending_outpoints: []+                        n: 2+                        tx_index: 2c7316f0b66bf4c5a2078cdaacd679d96c2249a32736ea7d3ce36af8b6a215c0+                        script: 76a9140c511401eb2e64aee8f1fc3dc49869b32819845f88ac+                        addr: bitcoincash:qqx9z9qpavhxfthg787rm3ycdxejsxvytu8zyrvhsf+                    result: -675+                    balance: 91015+                  - hash: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                    ver: 1+                    vin_sz: 4+                    vout_sz: 3+                    size: 703+                    weight: 2812+                    fee: 1557+                    relayed_by: 0.0.0.0+                    lock_time: 0+                    tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                    double_spend: false+                    time: 1613556294+                    block_index: 675214+                    block_height: 675214+                    inputs:+                      - sequence: 4294967295+                        witness: ""+                        script: "483045022100a3d05e493ed722b9c9519ea77d2d3c139d70bb117497d8e517fb\+                                8b5b196ceeaf0220608a93847c452c461ad841eabccc75e065c310e8ae93ad6e\+                                60953e6c089a297c412103b4c6da6474554411af1f6ed8deeeafab9848bc6bfc\+                                6cb716f135e7dd674f7a26"+                        index: 0+                        prev_out:+                          spent: true+                          script: 76a9141369d9bd3ea79896797c4143c97e9748cc06262d88ac+                          xpub:+                            path: M/0/65+                            m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                               ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                          spending_outpoints:+                            - tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                              n: 0+                          tx_index: 4890ab0fed725daa08879afcea302a4c7925c094c98676cab3edcaa186adbc41+                          value: 7000+                          addr: bitcoincash:qqfknkda86ne39ne03q58jt7jayvcp3x95u9mrv7ra+                          n: 0+                          type: 0+                      - sequence: 4294967295+                        witness: ""+                        script: "483045022100b2e36d340c3ba421f762ce1e4adde0a8c7cf550aa30022a4add6\+                                ac1003ea1a7d02200cd9d8ccba03a33eccd6f4108189962067597d25d969b9f3\+                                ab019212a0bb6824412102e14eaaa6cec9deefe5983dec61bbb2c9fc82dbbc12\+                                4d395d97d26494864f30ec"+                        index: 1+                        prev_out:+                          spent: true+                          script: 76a9148ecd9c021c8650db874940103f1f77e6da14d88e88ac+                          xpub:+                            path: M/1/19+                            m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                               ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                          spending_outpoints:+                            - tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                              n: 1+                          tx_index: 4890ab0fed725daa08879afcea302a4c7925c094c98676cab3edcaa186adbc41+                          value: 45732+                          addr: bitcoincash:qz8vm8qzrjr9pku8f9qpq0clwlnd59xc3cvrfxdpkr+                          n: 1+                          type: 0+                      - sequence: 4294967295+                        witness: ""+                        script: "483045022100ff406af352e0680f20c3efcf8a7915ee8c2f1322073fce5736cd\+                                c7d264576ad00220637cec6bd985f68395bc5a29e7c15fd8a5b74dafd169c2ae\+                                d86e453c91eb196a4121025b8a5938fad2f4e6cc7d7a32dd2b6a206a0082e2c6\+                                ecdd5ab5a15e855e1a2ff3"+                        index: 2+                        prev_out:+                          spent: true+                          script: 76a914bcd2b9001eea656e1a69f280cdc8dd86dbc8bf9288ac+                          xpub:+                            path: M/1/20+                            m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                               ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                          spending_outpoints:+                            - tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                              n: 2+                          tx_index: 671ba5996381a919c7b88415513ca1db54528e3741282a44228f7eea4d23ffad+                          value: 19515+                          addr: bitcoincash:qz7d9wgqrm4x2ms6d8egpnwgmkrdhj9ljg80neqzgv+                          n: 0+                          type: 0+                      - sequence: 4294967295+                        witness: ""+                        script: "4730440220445da9cea01832fbb4882b8aeb12189a91f32c9e0eec42c2b8ab0f\+                                5af40c38de02202378f8f3e774c3515e7082460c34faf5631dbadc0fff072084\+                                9aabcc70b41dc44121028c8ec4b2d812141f34ecc23ed6b533ee1c36e1654619\+                                05dae99595072529957e"+                        index: 3+                        prev_out:+                          spent: true+                          script: 76a91401b513b7affb4ff8ca96d1e25a78431687d7709e88ac+                          spending_outpoints:+                            - tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                              n: 3+                          tx_index: adbaa9b985968e42369f5dc96890480c84ee68537fb0c200a2a09dc3bdd6c2d3+                          value: 546+                          addr: bitcoincash:qqqm2yah4la5l7x2jmg7ykncgvtg04msncm9ta8v9e+                          n: 0+                          type: 0+                    out:+                      - type: 0+                        spent: true+                        value: 690+                        spending_outpoints:+                          - tx_index: 26ce9896f0f64349670735bdc30fb42adadac61348e403e78c5f92c3fb697355+                            n: 0+                        n: 0+                        tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                        script: 76a914be0322af705fa04d0550606dee421dbfc28d7d9988ac+                        addr: bitcoincash:qzlqxg40wp06qng92psxmmjzrklu9rtanyazwxtjqa+                        xpub:+                          path: M/1/21+                          m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                             ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      - type: 0+                        spent: true+                        value: 70000+                        spending_outpoints:+                          - tx_index: 26ce9896f0f64349670735bdc30fb42adadac61348e403e78c5f92c3fb697355+                            n: 1+                        n: 1+                        tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                        script: 76a914e53b8bc9d022aecb0cdd2f3eb874b6d563e73d8f88ac+                        addr: bitcoincash:qrjnhz7f6q32ajcvm5hnawr5km2k8eea3u5mcg7n9s+                        xpub:+                          path: M/0/67+                          m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                             ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      - type: 0+                        spent: false+                        value: 546+                        spending_outpoints: []+                        n: 2+                        tx_index: 0e81fa97ae5106f6e967de912908ee261473575e890e815fa78084f9d1b68382+                        script: 76a914c45f36d8156fa82e2f380452b0c42abd0a7aeedc88ac+                        addr: bitcoincash:qrz97dkcz4h6st308qz99vxy927s57hwmszhkl5hdv+                    result: -1557+                    balance: 91690+                info:+                  nconnected: 20+                  conversion: 1.0e8+                  symbol_local:+                    code: EUR+                    symbol: €+                    name: Euro+                    conversion: 170436.14609786443+                    symbolAppearsAfter: false+                    local: true+                  symbol_btc:+                    code: BCH+                    symbol: BCH+                    name: Bitcoin Cash+                    conversion: 1.0e8+                    symbolAppearsAfter: true+                    local: false+                  latest_block:+                    hash: 0000000000000000005d55b8ab4de4c864b815edf9fb52278d69a614c3c7c72a+                    height: 688824+                    time: 1621613356+                    block_index: 688824+                recommend_include_fee: true+                cash_addr: true+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/rawblock/{block}':+    get:+      tags:+        - Blockchain+      operationId: getBinfoBlock+      summary: Raw Block+      description: |+        A block with its transactions, by hash or height, as Blockchain.info answers.+        With `format=hex`, the block serialized as in the peer-to-peer protocol.+      parameters:+        - $ref: '#/components/parameters/binfoBlockId'+        - $ref: '#/components/parameters/binfoFormat'+        - $ref: '#/components/parameters/binfoTxIdIndex'+      responses:+        '200':+          description: Raw block with transaction data.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoBlock'+              example:+                hash: 000000001aeae195809d120b5d66a39c83eb48792e068f8ea1fea19d84a4278a+                ver: 1+                prev_block: 000000000845517b31c6820d83f25cff46429bf136a7515fe504116427e60f8e+                mrkl_root: 27f1d66f8a1ee5280f4e92508dcb647e954d53004905d08a75574daee4988360+                time: 1270916538+                bits: 472518933+                next_block:+                  - 000000001c920d495e1eeef2452b6d1c6c229a919b28196c103ecffebabee141+                fee: 0+                nonce: 58489010+                n_tx: 1+                size: 216+                block_index: 50000+                main_chain: true+                height: 50000+                weight: 0+                tx:+                  - hash: 27f1d66f8a1ee5280f4e92508dcb647e954d53004905d08a75574daee4988360+                    ver: 1+                    vin_sz: 1+                    vout_sz: 1+                    size: 135+                    weight: 540+                    fee: 0+                    relayed_by: 0.0.0.0+                    lock_time: 0+                    tx_index: 3395786307389866+                    double_spend: false+                    time: 1270916538+                    block_index: 50000+                    block_height: 50000+                    inputs:+                      - sequence: 4294967295+                        witness: ""+                        script: 0415112a1c02c100+                        index: 0+                        prev_out: null+                    out:+                      - type: 0+                        spent: true+                        value: 5000000000+                        spending_outpoints:+                          - tx_index: 2434377525635237+                            n: 24+                        n: 0+                        tx_index: 3395786307389866+                        script: "4104a9d6840fdd1497b3067b8066db783acf90bf42071a38fe2cf6d2d8a04835\+                                d0b5c45716d8d6012ab5d56c7824c39718f7bc7486d389cd0047f53785f9a63c\+                                0c9dac"+                        addr: bitcoincash:qr52xkse9lqlpmfuzlx2lshmx4qnchljns9fmxxydw+            application/text:+              schema:+                type: string+                example: "010000008e0fe627641104e55f51a736f19b4246ff5cf2830d82c6317b514508\+                         00000000608398e4ae4d57758ad0054900534d957e64cb8d50924e0f28e51e8a\+                         6fd6f127baa5c04b15112a1cb2787c0301010000000100000000000000000000\+                         00000000000000000000000000000000000000000000ffffffff080415112a1c\+                         02c100ffffffff0100f2052a01000000434104a9d6840fdd1497b3067b8066db\+                         783acf90bf42071a38fe2cf6d2d8a04835d0b5c45716d8d6012ab5d56c7824c3\+                         9718f7bc7486d389cd0047f53785f9a63c0c9dac00000000"+        '404':+          description: Block not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/latestblock':+    get:+      tags:+        - Blockchain+      operationId: getBinfoLatest+      summary: Latest Block+      description: |+        The newest block, as Blockchain.info answers, with the ids of its+        transactions.+      parameters:+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Latest block and list of transactions.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoHeader'+              example:+                height: 688824+                hash: 0000000000000000005d55b8ab4de4c864b815edf9fb52278d69a614c3c7c72a+                time: 1621613356+                block_index: 1621613356+                txIndexes:+                  - 4c38eb2b05ee5ed21d4f04aca264b6b5c2ecdebc463979880f34ff11d61b9df7+                  - 01823b17775de166d63f1a04cfb6dcdb758a864037808d8855db20e186db8a25+                  - 0448ff64f09a9b2942098edb92bf9ceb75277a21e4f3df5e33867b293aad7ec3+                  - 069023d56d47fa2c6f0f435b50fd160f7d2696ad73008753e344264750f48414+                  - 07145f7740f1204908194f9fec585b233d7b2b6dd260e43e4879f2affea02851+                  - 07ff00c1b6b058ee5823005fe05209f54ee71ed2059528a378aaa0bda9b8c2d6+                  - 08756d7b411d0ac4993f890fd216501944b1b4bc61df69ddaa9f721a4c75ec2a+                  - 0983b46978824feafcc7c67b5b0df164c81d8a71d9c02e0dd9b470d5f66c1a58+                  - 0a23d6bf0a8f7298bd422576d51eaab3e6a51e6b176424bfc61df48063b87a6c+                  - 0c08237a85fd2583958bfef936ef4c43784d0f13c57753e12492d304b7cf6155+                  - 0c090bd29a8b29f9f0ac36ad88c9ff0b5087f08ac51342de8a44f36bcfe20911+                  - 0cb873849e4f72fdab5c91b37bc3937c2c1b495349fa5424f57d4c9c72f62ead+                  - 0e5b552bffa5e732307f2c33ffbbc5ac8f523bfa991fd5a2711c8a5e698fe626+                  - 0f75524e1f8e0018275fb70a7463b0ac92f712485348bd0f0229baaf7149c039+                  - 1713d89ecaa24b8a82fee48740c032969a1be453608c760a36e9ac0c2925c568+                  - 1ab310ab552ce3a8103a565b9b63988898da47c0371d25cd51ad718c07173a9e+                  - 1c2ed964c3c5f825dfe67e3b1bd432cbfff712c69c94c9972cc790cd6d9690cf+                  - 1ffedd33d7df4777c4f623669b678a3a0ddda9649539d098522caa51ef4f8a61+                  - 2216e249cbf1eedc8fc525c74a64db4336da381cca724161bafbbef2cf713510+                  - 22a9d5a370cf548c2e5155109eca8e29c0cd88f5b16e2db8c2e63fa0a71c48d2+                  - 23e4aa5d99ee70c33dedd634435d208182f8b50f64a301766ba88c4143619cb1+                  - 26ad4b3701e7d23ef4cee7408a21393afe28ad03e4524788cc1fc18949a2a29a+                  - 270b9cc678b56a04aba36be6254acdc21ab11c9e3eb1ab6539595dcc35180742+                  - 29e14b7b36226f089b84778508434cbee387ac643a286a38f314d46649cabd2d+                  - 2b977eadd3052f2640ebfa33b2e602bea12af04bb30d7912634066159bafd738+                  - 2d283d80d8244bf4e21e2bf958b35d9946a7dacc6032b8ca75955ce649889619+                  - 33feebf0f5712ddaba475f4cf56cc10c33c1e8b6630885c1d16f0903b5f28f49+                  - 3b4749b05d4489fe3509a0eaeaa3bf84ebd18d9e899a4c0f93cec4906f0b4c72+                  - 3c445791dc93009e9e82a2cc7e1f063d2aaaba3541c79a5278e24b05d092f373+                  - 3e3e7b8c6a8ed50b44358d3501b4172cce9f7e6be2c3fa682cef69bc1b77ebbc+                  - 420d4e957e75a248a003349c6f36780bd7bac479af31e26d52bfc1580f1e05f4+                  - 43c659f05fe4bac95041ee8ad9cb487fb5c178f260ad111f79555c6208d465d6+                  - 448b6610021f4f74a7376da139167a85fc8fd34a9aa0c845841439a574676df0+                  - 44e23144d415417fae2d03f071621f7f87ffa4847a131b28e220238a82858384+                  - 45a34bf311e2544428a35924701a216306da67a1fd921a95520c130e164ceb40+                  - 45f96d25c71bac0f5026428b4a114ea4821e6923e7223fc9730e9cbacc52a476+                  - 47c83dde8a4ce80efcd63981f27fd90780f8f51694f7a83de66f15be201deaf0+                  - 47f43223ebded3e0397d143692ad93197d0c36fd2033cb6c21619cac26372783+                  - 4b86e1d7d66159eb795f059996ce2d25ae9b7adf1c4a08b7b77e3fd29d683588+                  - 4c237092cc3a1f6c3a6077aa929a53fa71879d607030db66158b899f6b691329+                  - 4c6370f1d46aea314c51d7221f436cc3d897d78c7de9b6413b17c7fa0d2d77e5+                  - 4c6fd1d42c445bd51c135ee3cecfb541d2467d40b2678663d56a39c20c12ced1+                  - 4cd70717f5dd726b19527837c58b1df094b83eda05c001c21a6fa9b9b8a6b9c3+                  - 4d5fd51538bfa055ed138ad83437ec1744186d71dcb10f2b668bc9ea3f889bde+                  - 4e84251545b58a7d76b6ec21973b99b317edcf6afc071bea7854a3c5b5a8d524+                  - 54dc82365400c51292ebe2a2351a172615527164c024b327688c435bf34a88d8+                  - 57e1c65a83b47dfdecadbecab3da05c65d0134ef997e88f5b9a478307be21cec+                  - 590d9ff2561efdfb2330ae736ca44486da720ef0d7f63168c9b5691dec93ab03+                  - 5b0cc365c8fa1256464f5bcb92ba937f778b489eab9b5903aa59ce35fb8d7ac6+                  - 5b565353fb2109962fdb7e550c7e3735890cedd45efd338d829a08bcff222224+                  - 5c46cc7305f0ca412bacf44c3f7c727aa55db0daeeb011b7d1042660563081df+                  - 60d2778361afe1230a0f8eafc2396d5cee8975fc13a8119e6205a61022ec9fd9+                  - 61b28c70c16c2e0dca2f8176bdb2eee168596bdb5c1101827f36c2a78e5963dd+                  - 63001aadaafa6f6c375368c653fbf38075f181f19d28e196879115b378fa4e0d+                  - 6787eb3bc645d7e1526ccb32efbf1e19c759c66559a8da2bbbb4b43ed4f2f101+                  - 687f32bd1f01e84bea246c9dbac219bf2904863d882d653bba1c13c1d4549e0a+                  - 6928119044c4718d2a146ee77a63d76d0f53d20a8c6d8166bb14c455e6db194d+                  - 6a8afc569208b5aadfe917eeb00cb63742b85f2bfb620e35480aa179c7bcc91a+                  - 6b19caf7d11225dc62bceba5835c5a9f676d5626138cfef7c717729758fd8120+                  - 6b73c2a0a8da9dd434eea8363a784fc4d3199266dc79598a15482519e15cd957+                  - 6c7fb02034c7768a0fd377823b90a03003be26973fc33e32cfe31709003dd0d8+                  - 73e99e7fd98eca88447bdc23f1f622c71eee973c24e16804431ca9540bdb3eba+                  - 75be45e58da8e1945d691dd0d6e37dd09e71bfa79aa3216bdb5d908523f68a0d+                  - 762eac92e4e6fe747c97c5894b2367a529e25774874afa0d00ba75c86a99a778+                  - 76e89ea97e8a465e1dd28f38620e755dface6b58fdc7466a8fefca0b172d14ae+                  - 785063b0221fddd9541395e636a440ecd03d20762371dedc73243c1ca835acad+                  - 789a6c7f59a852ab02974d7d3588e1d1f63850f320edd6d68cc8a20343b20353+                  - 791aaba586b112c7c40d503ec00d0cd092cfa11f460c2c7d03fb2b92f9d839ce+                  - 7b1a96d3dc25129876584fca0b354820e2315fc327c156e0074c824d1985ccbc+                  - 7b275d4724d3d79037e27a8ec2e6b9c25be2cc5d475f50f8e10f91c74b5836b5+                  - 7c234031e50aa853509fbe8eab5380bef517340064c9ddf7bd7529bda93265be+                  - 7cbee59f16df830c37ec51bc3f364731e17e71df95f743879aabfe3a5f10b401+                  - 7d79a6db1788061453f846d0a0f95dd0f6d76b223dd6f8eec0ad7154cbc6ff26+                  - 819e4070d42221831dc899b3ce1b8afadce75e2c29a7254a3704476729471a10+                  - 8621220b62b96f4fd16601b958536cb0574c9da0af8a9c0e0bfb88688ee82cb8+                  - 873c4755e36fabed41e9d340073536c6e328c243685b74f2dc6348f66d865458+                  - 94d9389300590814bf05309a141338ab0c096c16f469b3c64e8fd5e292fc35c3+                  - 98cafc73d825f0625e57f6a13513abead77f98d837f1a9ca41cf846f0a746d73+                  - 98ff1ea70619428f049acbfa914fc672b6fafabf754f3d8dc6368a2d5e7f169c+                  - 9cc4658b94a2c294f599fc5b621e0a718ef4237075bd31ee354e786c577fc9dc+                  - a258b428c7ff1d3fe238262b9c5c041cba8b90615a31df2073c6bf7730b50558+                  - ab50430d07205a389be9d7b48a5f1a8876fb362db7b1255e1c24543f8d07b57b+                  - ab7ed19e04e02b02652bad917fe47b08ec9ea5136ab83aee9d735e58aafa95cb+                  - ace73995984c36e16ef4eeb01cb0e92d361845f3d96bfa37d89404b8d56f4458+                  - ae2ff184a71aa3ac315d8df3b7575b221a96a65d5b7e821e28bc51d2b79a7728+                  - aff7771f7d855f3fdb82eed34d6bc2425726ae31050d19d84670b5f1a0cdb581+                  - b1afdb5db3855c10a3ab88bbcdc30a43abdc1b3358c3a3219c5dc23b29a164ac+                  - b994ba455aa12155589aea3dd37dc1e7de4a4075db90af3b39607d72727894b0+                  - b9aa95a14fe6da28fd0f5b25b1ffb3837aa0645b868e120b5d81f08376d2946e+                  - ba916c099c4595fa0e5629228e6339e7a9a516657ec0178af8cec7401ab99571+                  - bbf377dc25d8178cca37defea3fbbc1219c55c4988502eae61924f4e8c7b15c4+                  - bc0da2b7f532c318a0c5f087c65de8e175f11218178261b48e4b5a34237e307c+                  - be80998651ed346b57dde55b62daabf3f2b7ab1fa23949d74206eebf9e0dddbe+                  - beb4dd6722a980a68215f44824d71c68e85d476916f191e2b9b64aba70df5011+                  - bff3d9c73b79f2df57907f9585962be5005de77a38dcafb4ed45b2d767bd5f9c+                  - c16c7991aa4843435234ea2d430a2c8cffa89f9df7d6da573c766f90f1b02fb2+                  - c1e102bf7cc4fad1d653cf8bf2f6dfdb56cc15c90eba48826f499748ffbb453b+                  - c467c9e1597fec2186261553669f3a030bc1fde0d71a51a7138a62007dd45d26+                  - ca007fa6a1dd1cc19cabdfefed711868a903f945a44c23a736f8508fc595e779+                  - ccab8fbc8cf6e22fc18142ce528aa4f9d513699e1c9d5acbd085c6c2500ff621+                  - cd959b2aba269c1132b7250edc34a9ef85fe13bf6c78285c3511b97b8b7350f9+                  - cdff2a8b1ca912c733e45ab44a2b3f97962076e4a60996b6525f4a4fd3d701ec+                  - d0d28136d03f5bac5fa4cc9479a3ad7ad76ffc3e731abe061149be8e19d3a1f3+                  - d649b5579ffe5ad98ff93f0715a91ccf1ebf60a564cefc01baba969eb6fc024a+                  - d67df8392a5822c420ee057ac19c6b123fb514853e43d40173f84c9853f54835+                  - d8bab30bd3cefdbb2ccbbe94efd22387584373256e26fd914fc3a4e7fdb06aab+                  - d951327e966ae8bede3ec5a0dceef158479c40dcb5805fe291e7a9149e84c418+                  - da74f9ec7c74131689bca50d48a8b3eabd6676668d828b74782d2f5e386d10e4+                  - db0666026842647e0de6cf67fa873eab94b541995daaec66af0169aa65186dbb+                  - dc5a07a8f0e3ee69a21c4da7f7ca3220154930c9d45c497ec738fdee2e9e79f0+                  - de629914e7be3f2f40ad87f70f6aeaca08e45f8c8031c2e587e48457ff05b33e+                  - def688cb3656706f4bbc98f0b23ef28c45de111e4171f5ccea872f15bfdb3606+                  - e1706c7cedf8dc991e52dffcd7d13f968471cce2e448532dfc4b42176111862f+                  - e3e332c6817077f6cb79253bb5ce2a680f097f7d85fc2d544738d6bf7ffc0c77+                  - e42d25d5fd76c7ee79794c2f3601b5616c518f54ce89f6c016fa10a6b41af055+                  - e54814a2125e258680824bcca3968eb7b9004bba955db3921326112050aeb366+                  - e55057e4e52a0ec20f4e00462f2974ac46ace4b31ba7b8764a4f8f371787f72f+                  - e5c9ed1d58b2a682219960ee90fd662519023913e741ea7caa5fdd6700890cd2+                  - e938943a6e66089f6200ffa6c211ec219beaaee383c3ac73c9cc15ccc9b67a6e+                  - e9af46657882c81d9280515f3db830628af7acb54d4598832f12081b9a2aab4b+                  - ebcb08f6a064672ee79d97949ee4ad783490cac01dba3188c02b457603882cd0+                  - ec0e5b73cf988356cf3d7140177c456811949cc610690537c484f37bfc586fe1+                  - ed4d51bd70497507a836f76d111b09ef659a912b7f2c34c4a85c83718603cdad+                  - f2a745c03817fb06f8c734eb84a535476a01ee3fca7a6eb4b58af06919a194c9+                  - f36881938c4081d5c3c07ea7c472436010a9bc50a272e5f98f2ba0c5be4541eb+                  - f5dd09f486fd31bb4ca2455b1ded267daf5b6b4afe77496e2f832537aae25ea2+                  - f5f93376df7b16aa5724b0999b7e512f1daf65671c952d544dce66d832ac7ffe+                  - f9bf3c16de953a3e4992c278706fd7ff7776da6b6c6aa14dfe8db4461cd04243+                  - fc9b718701edecf2c4031c7043a510c2cb9233138e2061caa8b18327f7d1a2b4+                  - fca2aa70b428bed7ed7d463c880491e720b6a46ad649601ed9a25048ab72dac3+                  - fef4d9bff10845b426cdb246c5450207a947cea02b014ec8523da5d8ed987a58++  '/blockchain/blocks/{milliseconds}':+    get:+      tags:+        - Blockchain+      operationId: getBinfoBlocksDay+      summary: Blocks for a Day+      description: |+        The blocks of the 24 hours before a time, newest first, as Blockchain.info+        answers. The time is in **milliseconds**, as Blockchain.info takes it.+      parameters:+        - name: milliseconds+          in: path+          required: true+          schema:+            type: integer+            example: 1304208000000+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: List of blocks for a 24 hour period ending at given timestamp.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BinfoBlockInfo'+              example:+                - hash: 000000000000475e465114fac64a267ef55a79f0431af774f0ec90d42ea1f2d6+                  height: 121126+                  time: 1304207878+                  block_index: 121126+                - hash: 00000000000037351a4fa2e4cc97852cb4a498419844d79fb29e119c6d4cb32e+                  height: 121125+                  time: 1304207785+                  block_index: 121125+                - hash: 0000000000006429474c3f6635636d3824f68841935debde15a763bce02a7c18+                  height: 121124+                  time: 1304207337+                  block_index: 121124+                - hash: 000000000000223b64b7083652f2c3c79b0e5f175bd885697377a39e79502297+                  height: 121123+                  time: 1304206894+                  block_index: 121123+                - hash: 0000000000009487db37d79be368408e08196c327e124a033ae164db10753dc9+                  height: 121122+                  time: 1304206476+                  block_index: 121122+                - hash: 0000000000004d8ce1babf4fdbf153f813ec4776ea53f7e36cd4897886349f0d+                  height: 121121+                  time: 1304206238+                  block_index: 121121+                - hash: 000000000000538fd5bb2e5e98ca0019eb5d121fb429021cefbac1413fe56ccd+                  height: 121120+                  time: 1304205547+                  block_index: 121120+                - hash: 00000000000049fdef5f0074a089e9f82bed072d5e6d975fc411cb1ba6d64c00+                  height: 121119+                  time: 1304205476+                  block_index: 121119+                - hash: 000000000000555ba01c35729fd735cc063ef7900db77cddca3656db755be830+                  height: 121118+                  time: 1304205252+                  block_index: 121118+                - hash: 0000000000008264df3eee52f65383383e4d852d2636718632f8b3b9a1cefe42+                  height: 121117+                  time: 1304205083+                  block_index: 121117+                - hash: 0000000000001f04dc63a2c8965e8ed033aa4f51a3d57905814f883a6f2e5590+                  height: 121116+                  time: 1304204752+                  block_index: 121116+                - hash: 00000000000007cdee3c6a4aa379d4e8464692882572a4f9e71f5e2401752284+                  height: 121115+                  time: 1304203893+                  block_index: 121115+                - hash: 000000000000560f1d2a1da96a60bc7f57a35d31686fadf2ded8a71347f049db+                  height: 121114+                  time: 1304203470+                  block_index: 121114+                - hash: 00000000000080e7dbe1831f5ad5190ff80da15196829bec92ece2c7b577f36b+                  height: 121113+                  time: 1304202421+                  block_index: 121113+                - hash: 0000000000000cd3ff350131f2879af7917390825cd027150d05beb8147582c3+                  height: 121112+                  time: 1304202114+                  block_index: 121112+                - hash: 000000000000220aec71a1052b33d11444db6f88e3a6e6313159c4609d0b92ca+                  height: 121111+                  time: 1304200737+                  block_index: 121111+                - hash: 0000000000008c32948edaeec42a90afd6cebb14d4e773e1b1ec5b4102d8bc52+                  height: 121110+                  time: 1304200549+                  block_index: 121110+                - hash: 000000000000876ce07b94635e3a8959ae23bda70a0ae1965998f4830ab2095d+                  height: 121109+                  time: 1304200510+                  block_index: 121109+                - hash: 0000000000008ce13ce258833d3551bf5031f08ee9fae3d1ffaad4e9967cad90+                  height: 121108+                  time: 1304200222+                  block_index: 121108+                - hash: 00000000000067ac45b2c3e1e5bdf1bfd04442adf0931943f9be47ba81c42b17+                  height: 121107+                  time: 1304200205+                  block_index: 121107+                - hash: 0000000000002384a7723c2e09e9b20a3ca23773d1539d64494ea06a1637272b+                  height: 121106+                  time: 1304199799+                  block_index: 121106+                - hash: 0000000000003da280d8a0a9689a96812b0ab29ac807eb814809e4ee78fd7576+                  height: 121105+                  time: 1304199563+                  block_index: 121105+                - hash: 0000000000005f22c5b947dd24621e55a24fb52cfe445c50836bbc8017ce4ef4+                  height: 121104+                  time: 1304199026+                  block_index: 121104+                - hash: 00000000000073011e748bfd2d6307285cbe28568a613ac9f8115e8fb1f1bd11+                  height: 121103+                  time: 1304198789+                  block_index: 121103+                - hash: 00000000000038e334a9c559b7f49cd76fb8f2d99537be7fc8d1cf69ed01bfdb+                  height: 121102+                  time: 1304198658+                  block_index: 121102+                - hash: 00000000000095015156b62244ae673fa593f901c61904ab112928bac95525bb+                  height: 121101+                  time: 1304198316+                  block_index: 121101+                - hash: 000000000000177ef47ee4d1b9ccbe1f18e5b58ad0c4c067c889a149486dd327+                  height: 121100+                  time: 1304198133+                  block_index: 121100+                - hash: 0000000000006f134512ebd1d7f8049ec2922045219227883bef6875ca7dad07+                  height: 121099+                  time: 1304197977+                  block_index: 121099+                - hash: 00000000000079458e8d4b02e9592e68990807028b57a6e604504820a4c956cd+                  height: 121098+                  time: 1304197956+                  block_index: 121098+                - hash: 000000000000941e8f34fae1ae47cee6f0232aac4bed2651f42ceb9c274940b8+                  height: 121097+                  time: 1304196739+                  block_index: 121097+                - hash: 000000000000013f70963dae6aabc2567494e4df00b77b21de27f9f7d51cda37+                  height: 121096+                  time: 1304196601+                  block_index: 121096+                - hash: 0000000000005678a39dbea710b9afc0d270d9a26cc9be9291bdc07957e53c8f+                  height: 121095+                  time: 1304196158+                  block_index: 121095+                - hash: 00000000000015b35809c71bf09ee6fa1273d9aa7912c7f73fcfdec983ac8616+                  height: 121094+                  time: 1304196111+                  block_index: 121094+                - hash: 0000000000006dba6d8651749fe59df7f784ba02a3af389e0d8de87556a7f2d9+                  height: 121093+                  time: 1304194495+                  block_index: 121093+                - hash: 0000000000000c347a3da7bfee84419ee0442483ad6bc398d447197381e1b435+                  height: 121092+                  time: 1304194427+                  block_index: 121092+                - hash: 0000000000001f50198f4121b99f8cf411a3677357e1963c96f37454aa4b91ff+                  height: 121091+                  time: 1304194291+                  block_index: 121091+                - hash: 0000000000003e5942271bbecdf8f2ea806b10f174fdcf4216c80cc96853ceae+                  height: 121090+                  time: 1304193527+                  block_index: 121090+                - hash: 000000000000983fa6142a7729efefa0cb792af99aa2c8fab9896a1556ba512b+                  height: 121089+                  time: 1304192511+                  block_index: 121089+                - hash: 00000000000077f33539182dab2d04bebdc6c1d74ccc7f9f7d4329575967a833+                  height: 121088+                  time: 1304192188+                  block_index: 121088+                - hash: 0000000000003db3d299a94b459863fdf9eb18f671f3d8bb16d94bea54ddd575+                  height: 121087+                  time: 1304190190+                  block_index: 121087+                - hash: 000000000000369f02d4c94f18ae5681b4286b7170a9193c85b5647ebc544c63+                  height: 121086+                  time: 1304190025+                  block_index: 121086+                - hash: 000000000000931f5591dddf392068a34ee7332943a04ad5925414e038882a33+                  height: 121085+                  time: 1304189376+                  block_index: 121085+                - hash: 0000000000007d65cd99d0711480f26aba3ba5c02a4a40360eb0d4c09ef8b1ab+                  height: 121084+                  time: 1304188567+                  block_index: 121084+                - hash: 0000000000006fefa18e7c8941b3829c17579757bcd5f5165af0fdaeaca29ae2+                  height: 121083+                  time: 1304188375+                  block_index: 121083+                - hash: 00000000000043cec6cd5155c65ce226db650faf2c54878db4702505cbdbbd4e+                  height: 121082+                  time: 1304187578+                  block_index: 121082+                - hash: 00000000000060316bb5b36d752d49890ccbb73a184f7cbc060680b6933584ef+                  height: 121081+                  time: 1304187368+                  block_index: 121081+                - hash: 00000000000068f040c2cde5c82894727b33610de4c05ea1b1233d2aa2fca6bd+                  height: 121080+                  time: 1304187123+                  block_index: 121080+                - hash: 00000000000028c05d4651de60cd3b44b5be2b1816243d8d965528a81ae7875a+                  height: 121079+                  time: 1304187008+                  block_index: 121079+                - hash: 000000000000472bd5cf01798c1dbd0e6d25dab52f8d696753580008db80e893+                  height: 121078+                  time: 1304186628+                  block_index: 121078+                - hash: 000000000000372744c19efaf112182bbed961a0fee0d89a6f04b4984e4f4584+                  height: 121077+                  time: 1304186553+                  block_index: 121077+                - hash: 0000000000003d4b7b5da478538beb0060ca553c54b35c8946f9a5ccb24512e0+                  height: 121076+                  time: 1304185981+                  block_index: 121076+                - hash: 000000000000033dcfc64fde1d82e1d8bc5c58e27a8f8d13c0bf4cba5d9d2adf+                  height: 121075+                  time: 1304185447+                  block_index: 121075+                - hash: 000000000000247a36e8f3abf31335721f15c2444f627e0a248671b9c907f7e5+                  height: 121074+                  time: 1304183466+                  block_index: 121074+                - hash: 00000000000072cb4cdd156e0eb5136f8c6c6b575f785c22949608236508f7ca+                  height: 121073+                  time: 1304182511+                  block_index: 121073+                - hash: 000000000000444ae09cec4ce9f07fe7b32cef925ec85b0891352a8ce4b1451b+                  height: 121072+                  time: 1304182067+                  block_index: 121072+                - hash: 0000000000001c6430c1997d19f4feaacf39dae96097255faf9fd20281cfa69a+                  height: 121071+                  time: 1304180846+                  block_index: 121071+                - hash: 0000000000001fc948f18a1a71312ad0cf8dd9163e4b5b4fab8c440915af16f7+                  height: 121070+                  time: 1304180836+                  block_index: 121070+                - hash: 0000000000000adbec0c504df39383277cfcde45965a598381226c9d69ac49d4+                  height: 121069+                  time: 1304179491+                  block_index: 121069+                - hash: 000000000000418f02482273ac6a32d16edec9b32ab0ba4df6cedace84296896+                  height: 121068+                  time: 1304179436+                  block_index: 121068+                - hash: 0000000000000d80f675420be668f669c8fe2fb47dd37973bc03acfdb65f85b8+                  height: 121067+                  time: 1304179294+                  block_index: 121067+                - hash: 00000000000040b5f954e6fafcee88e83bfc518652e38c4ddf0093fefbff180a+                  height: 121066+                  time: 1304178353+                  block_index: 121066+                - hash: 0000000000003fda5b8545f616c8669068ec0d55df9a2ab7c3eef56f1d27c7b2+                  height: 121065+                  time: 1304178052+                  block_index: 121065+                - hash: 0000000000009366035bfaac09768eda7c6c00f30dc43a82f242b03cded2af61+                  height: 121064+                  time: 1304177919+                  block_index: 121064+                - hash: 0000000000009149970f8b22ebc89bc39f94ecf62fb2d103895b140cd1e6929c+                  height: 121063+                  time: 1304176797+                  block_index: 121063+                - hash: 0000000000002fd2fcb88fb3161f8b1dc957edec768a92197a2682ef1e411111+                  height: 121062+                  time: 1304176684+                  block_index: 121062+                - hash: 000000000000423ab1d771c156011400c26796aeaa4a696e50883110d106e189+                  height: 121061+                  time: 1304176496+                  block_index: 121061+                - hash: 00000000000051f16c0a2856278559367d52f78e2edbe4e1209665899f504cb4+                  height: 121060+                  time: 1304176203+                  block_index: 121060+                - hash: 00000000000031a668ff1516b7760192175e11493a783cfc1fedced3fdf1ec68+                  height: 121059+                  time: 1304175932+                  block_index: 121059+                - hash: 00000000000024d6fbd7eb65351e6ed7891ff1adbcaab04ab806a623b375d702+                  height: 121058+                  time: 1304175282+                  block_index: 121058+                - hash: 0000000000008e60cef7434121c415367e71d331f66a7391c95d21eafed494f7+                  height: 121057+                  time: 1304175040+                  block_index: 121057+                - hash: 0000000000008e6ff594414d2ca8c59f7165664c97f46380e7fc7549f93850cb+                  height: 121056+                  time: 1304174913+                  block_index: 121056+                - hash: 0000000000008290778cf07c8352a105667c87209e0acd330efb871ef75add7d+                  height: 121055+                  time: 1304174863+                  block_index: 121055+                - hash: 000000000000443b9bf8b6471bf1ffd7d6cac5925bf6c4225eecd0ae9e7291e6+                  height: 121054+                  time: 1304173391+                  block_index: 121054+                - hash: 0000000000003795781dafa7d1634569ee4f553da536b34f5dbe24f381f3e0a4+                  height: 121053+                  time: 1304173084+                  block_index: 121053+                - hash: 0000000000001e35337c744b329dd4149732a5e72600ad8d3b16e541da0bbfeb+                  height: 121052+                  time: 1304172896+                  block_index: 121052+                - hash: 000000000000563c5e4e43915fdf15e5d64edff4f376d6293b1cd72078d563b7+                  height: 121051+                  time: 1304172724+                  block_index: 121051+                - hash: 0000000000007bbb29af8f47ac5f262af8b5eee80e33fbf450525e2bfcfa36b7+                  height: 121050+                  time: 1304172668+                  block_index: 121050+                - hash: 00000000000096640555f4cab1cfafc51cd71637fde69145d06eca19524eaeda+                  height: 121049+                  time: 1304172150+                  block_index: 121049+                - hash: 000000000000200bae132975386a7dda629616a7a0e64ee9d140510dae7d8e7b+                  height: 121048+                  time: 1304171784+                  block_index: 121048+                - hash: 00000000000012b170573c74b308a605eccb4bc7ab7bcb0b3ba83534ba9133a3+                  height: 121047+                  time: 1304171467+                  block_index: 121047+                - hash: 000000000000597cbe5b6e79c61486d44370f163da0d9fb6752ddf4d59f7d5b9+                  height: 121046+                  time: 1304170765+                  block_index: 121046+                - hash: 0000000000001071171398c29d9ef827e1bf53378cea6a963c9b3f129febbf1b+                  height: 121045+                  time: 1304170541+                  block_index: 121045+                - hash: 0000000000003f8500cd6c42ecff802315281fd87f80af54c5ec2b063d771fb2+                  height: 121044+                  time: 1304170240+                  block_index: 121044+                - hash: 0000000000008900678ecf84aaca49f1274ea64bfacbe0b1edd08f6a8f0bfc39+                  height: 121043+                  time: 1304170185+                  block_index: 121043+                - hash: 0000000000005640d1cecde32f56f82200507aa2ccf1ed044a3d849a0fa26a6e+                  height: 121042+                  time: 1304169993+                  block_index: 121042+                - hash: 0000000000003784a99e57cb832ea4ff50c0c2ccb6e69a22fbfc6a752f2d6170+                  height: 121041+                  time: 1304169545+                  block_index: 121041+                - hash: 00000000000072353d12f50a216d98d8b6080ad09210e32cef475f018d8f7648+                  height: 121040+                  time: 1304168718+                  block_index: 121040+                - hash: 0000000000006b7e67295ede5da1a1185268367b092ac569cb74a32be6f75b59+                  height: 121039+                  time: 1304167964+                  block_index: 121039+                - hash: 00000000000050f019b53f0d9378879f30fe38cb73d614fcb600809ca447236f+                  height: 121038+                  time: 1304167287+                  block_index: 121038+                - hash: 000000000000056017a46b9eb7543a9f99651cf93e764d52aadf2dfad7071d05+                  height: 121037+                  time: 1304167052+                  block_index: 121037+                - hash: 00000000000043e5c795a4d21051d4281e303526bcc552302441be294b0da0a8+                  height: 121036+                  time: 1304166837+                  block_index: 121036+                - hash: 0000000000005c89ac2411fe50f9b0ccaac59ea566561a02e4ae0dc4289244cb+                  height: 121035+                  time: 1304166380+                  block_index: 121035+                - hash: 0000000000008ba010cc430bece41b71417b7ed4102dcb4b5b3ba759231e909e+                  height: 121034+                  time: 1304166346+                  block_index: 121034+                - hash: 0000000000005a6e318a7cd5fda64ffa3c21688afdb92b2ba75208ac283222f1+                  height: 121033+                  time: 1304165870+                  block_index: 121033+                - hash: 00000000000054a4313bcabfc559bdeddfa072b47858d35c18f3f128b9a8f827+                  height: 121032+                  time: 1304165849+                  block_index: 121032+                - hash: 0000000000001a2585f316f81b20623e693845515f216f3ecb787583f4b79874+                  height: 121031+                  time: 1304165435+                  block_index: 121031+                - hash: 0000000000002df0d65141941ff46360950bd6d59e840e7543d7b1a02a587cdf+                  height: 121030+                  time: 1304163816+                  block_index: 121030+                - hash: 00000000000021116f3a21c0ffa0d44173274b53bddce4e3e3619d18a7e65036+                  height: 121029+                  time: 1304163476+                  block_index: 121029+                - hash: 00000000000072a74cc890fd65a1f1fc870e53ffa3bad2e2ed366316ae46533d+                  height: 121028+                  time: 1304163417+                  block_index: 121028+                - hash: 000000000000555d687591c8ee63ab8483166688e529eee9e09f1ccada907ede+                  height: 121027+                  time: 1304161895+                  block_index: 121027+                - hash: 000000000000148b26490d48f10bea50ee749ee78d06cb8a4bd7a664e4e5556d+                  height: 121026+                  time: 1304161868+                  block_index: 121026+                - hash: 0000000000006b01c6bcc0c42fb60dd8ffb3abc3c43c77acddf51968439b96d9+                  height: 121025+                  time: 1304161256+                  block_index: 121025+                - hash: 00000000000098d028553328669d0d32593c084d28dd2a000d90e486d32f6f91+                  height: 121024+                  time: 1304160847+                  block_index: 121024+                - hash: 0000000000004eae95bb184dacd3fe1e272e4eeb8e2c6dfeee3314bd88901470+                  height: 121023+                  time: 1304160368+                  block_index: 121023+                - hash: 00000000000033b9d36ac45cba7fd17ce0a641c88175a4e3b2c9f0a8cac22f58+                  height: 121022+                  time: 1304160199+                  block_index: 121022+                - hash: 0000000000008fbfc9fec3ea613bae59118c86eddd3e0df1ef645058c3998c3b+                  height: 121021+                  time: 1304159323+                  block_index: 121021+                - hash: 0000000000000a93ce027acf500aa294e2c6ff58e74499bde8070e848135adf4+                  height: 121020+                  time: 1304158552+                  block_index: 121020+                - hash: 00000000000069cbf9663a3f1dd07f52eabc55c9432310ca9ef4490bd57243a0+                  height: 121019+                  time: 1304157443+                  block_index: 121019+                - hash: 0000000000004441bb7e8d53adbf047a281a06dbde22a79b05b0e7a2bddc9f9d+                  height: 121018+                  time: 1304157425+                  block_index: 121018+                - hash: 0000000000003549dbf901d348ed95e2e920e7311c304b109f8e6791ea6c4853+                  height: 121017+                  time: 1304157199+                  block_index: 121017+                - hash: 00000000000088aafd531b7c10e0d3cd2e5f9fad0870786eaed442194b9b12e3+                  height: 121016+                  time: 1304156123+                  block_index: 121016+                - hash: 0000000000008ebdfb11ffc304957ffd46bd34fe8f958fa3bc485992a24478c3+                  height: 121015+                  time: 1304155401+                  block_index: 121015+                - hash: 00000000000043c8d4081d8259ab0d45b696f2b920ede34aa41e359aded77662+                  height: 121014+                  time: 1304155199+                  block_index: 121014+                - hash: 0000000000002ae7ed932ab9bab081de9d7aa7d5834e24a33707ade32cab328a+                  height: 121013+                  time: 1304154121+                  block_index: 121013+                - hash: 00000000000065342830dd781b0228588bce6783e14153b759c1f07044fdf90b+                  height: 121012+                  time: 1304153720+                  block_index: 121012+                - hash: 00000000000025adfa13b670aa30dcbfbd188807dcb586e92d671ea14561d333+                  height: 121011+                  time: 1304153294+                  block_index: 121011+                - hash: 00000000000069e01c12dace34a54f202593dea75c729297e512084e87d3b4c1+                  height: 121010+                  time: 1304152433+                  block_index: 121010+                - hash: 00000000000088b7820a82e16e46533399bad43f33c1bff76e3ab31fdf9babe7+                  height: 121009+                  time: 1304152394+                  block_index: 121009+                - hash: 000000000000495dfd2d08a2985b53f18cb4f0cf35500a4f5b1433ca0c8924c1+                  height: 121008+                  time: 1304151121+                  block_index: 121008+                - hash: 0000000000003c08e84cb8e3b12abee5b70bc48d16841b4ecaa1e2fc94a7fcf6+                  height: 121007+                  time: 1304150249+                  block_index: 121007+                - hash: 000000000000904daae12718c3166c646e564cf27878992b5de64ddac87d5273+                  height: 121006+                  time: 1304149962+                  block_index: 121006+                - hash: 00000000000055880368265ceddf3d540af70c46c20de79b4f23f7300a1f8341+                  height: 121005+                  time: 1304149231+                  block_index: 121005+                - hash: 0000000000004d471350cdbe5f5aba72d6441364ffdfa2a9553ec86ea4caec64+                  height: 121004+                  time: 1304149175+                  block_index: 121004+                - hash: 000000000000685609514052e0715eba6a1e7c0deaf59a24315c259e9fbc1732+                  height: 121003+                  time: 1304147388+                  block_index: 121003+                - hash: 0000000000008379f91db9289063f330e39a6b480723b86abe0bcbd2f0d46628+                  height: 121002+                  time: 1304146407+                  block_index: 121002+                - hash: 00000000000087390012d022582eeb5abe18f817b11dd9a89be678c0ea293dde+                  height: 121001+                  time: 1304146354+                  block_index: 121001+                - hash: 0000000000008addb06debe0ba6b11e220b14b2b531750fda710b62bb1d3d706+                  height: 121000+                  time: 1304146291+                  block_index: 121000+                - hash: 00000000000075216989a1ad8b35d434f9aa4fa3bb5b998da8f422ee53923d9b+                  height: 120999+                  time: 1304146163+                  block_index: 120999+                - hash: 00000000000091a7063abd88ad281dc11f88fbc7e3766a831ddaee151470ec57+                  height: 120998+                  time: 1304146157+                  block_index: 120998+                - hash: 0000000000003160d4319a2d75851bb2c7adf63b68313bc63753d86cb5138a40+                  height: 120997+                  time: 1304146052+                  block_index: 120997+                - hash: 00000000000006d844c0d73ab70de03066f4d3c6143246dc15ea1484dc9cfc81+                  height: 120996+                  time: 1304145624+                  block_index: 120996+                - hash: 000000000000127eaca1e5dca20b151356197fffc51e19c7735427f986568a81+                  height: 120995+                  time: 1304145067+                  block_index: 120995+                - hash: 0000000000001bb36473406f41a0018376d1e3686c956fd0bb72013007edba39+                  height: 120994+                  time: 1304144388+                  block_index: 120994+                - hash: 0000000000004b741ac6b5514fd4e19c8ed1ff3b78bb80bc0a6eed7ed61e6822+                  height: 120993+                  time: 1304143780+                  block_index: 120993+                - hash: 000000000000941e65511aa5408d5db8a0140a686bd01e64bbdd34d5c7831246+                  height: 120992+                  time: 1304143492+                  block_index: 120992+                - hash: 00000000000091fc5ae72d2c5a3c32f9e31206fd0bc9f6d3c80a9fb614dcff1e+                  height: 120991+                  time: 1304143441+                  block_index: 120991+                - hash: 0000000000002f283c01d88eed0b948fab4f885ef39c66b109d5a17fc549e42f+                  height: 120990+                  time: 1304143399+                  block_index: 120990+                - hash: 0000000000002812eecd335460ff3c2171d11df51989d247460e6e11ab8bef6e+                  height: 120989+                  time: 1304143270+                  block_index: 120989+                - hash: 0000000000005aef4b7065038db827431088b52216f1a5249692e7046223a071+                  height: 120988+                  time: 1304143015+                  block_index: 120988+                - hash: 00000000000062033f6c4ed1e44e1538a6c543c8937c030bccdb4d368eb62ceb+                  height: 120987+                  time: 1304142945+                  block_index: 120987+                - hash: 00000000000062c28dfa926828d6b7b43bac3366a153542bd9e7f156d882b34c+                  height: 120986+                  time: 1304142356+                  block_index: 120986+                - hash: 00000000000027987257100dd3c6337e3d41391f4736a9bdd1721a2108111e87+                  height: 120985+                  time: 1304142114+                  block_index: 120985+                - hash: 00000000000000bb19cf058b82c71b021f0de98ac1ae36315396b0e65805e9bb+                  height: 120984+                  time: 1304141057+                  block_index: 120984+                - hash: 0000000000004334e532084c786d0e15fbcd10ed096784319cf8944d973fb97d+                  height: 120983+                  time: 1304140925+                  block_index: 120983+                - hash: 000000000000448253842df0ae6bedb71af40ff5dbf4f8f9443471361fc3b6f6+                  height: 120982+                  time: 1304140819+                  block_index: 120982+                - hash: 0000000000008ba2594881b59f927b618d7539b44c6315dabe9173d237f2079c+                  height: 120981+                  time: 1304140628+                  block_index: 120981+                - hash: 00000000000079f504aca0b771e37eb71d2ea263acf38c7bab77f8ad58e7c104+                  height: 120980+                  time: 1304139778+                  block_index: 120980+                - hash: 0000000000002b5cecc48211794512bffe632337a10bb8dbf5cb5a82f386aa9f+                  height: 120979+                  time: 1304139554+                  block_index: 120979+                - hash: 0000000000003648d4aff647ea4e4b61bb6ee8c055a0be09c5d10252d7aa2532+                  height: 120978+                  time: 1304139523+                  block_index: 120978+                - hash: 000000000000609d4147b41cead0f8a7d966b6e721e0585c6257ebe6caddf45e+                  height: 120977+                  time: 1304139435+                  block_index: 120977+                - hash: 0000000000003a8c92682547fbf0acab4cc2223192d77f6df8e437858b49b86b+                  height: 120976+                  time: 1304139383+                  block_index: 120976+                - hash: 0000000000001dcc62bca24efa6271c4c9ffc5e60067a3c2ad64e352fd13f2d3+                  height: 120975+                  time: 1304139145+                  block_index: 120975+                - hash: 00000000000026bc82ee2da9b2a7a240aae8a9112bc81a78895f1d138d9a1db5+                  height: 120974+                  time: 1304138241+                  block_index: 120974+                - hash: 0000000000000e011816b36f39b1e0b84682e84720e79ad58643e741b295b060+                  height: 120973+                  time: 1304137450+                  block_index: 120973+                - hash: 00000000000086bdcbbb0c4a66b999fdff516717d22ccfda08ea1592561ef37f+                  height: 120972+                  time: 1304136499+                  block_index: 120972+                - hash: 0000000000000edd47e67c32af711f0e14ab50d6a83854df2792152dace03a26+                  height: 120971+                  time: 1304136159+                  block_index: 120971+                - hash: 00000000000017a29a60c514c4cadd52dd95ae171a324f9805fa5491d27c1611+                  height: 120970+                  time: 1304136043+                  block_index: 120970+                - hash: 00000000000006d46cba774fa65a02486974b85442dab196b55c550fd6896726+                  height: 120969+                  time: 1304135319+                  block_index: 120969+                - hash: 000000000000922d45711f822fc486373d5754d7033730393e93755f8e7af95a+                  height: 120968+                  time: 1304135196+                  block_index: 120968+                - hash: 0000000000005bcc470f7d6fc5d4cb43526e16811413a7a6dd1ec06a6f7e9ed2+                  height: 120967+                  time: 1304134362+                  block_index: 120967+                - hash: 0000000000007251370503ea542f688e576d3d48a6355e14ab71a0f4d132b9c6+                  height: 120966+                  time: 1304133971+                  block_index: 120966+                - hash: 000000000000135da63a64e0f50210b8701ed4bbb2fc0fee1bb51abe65ea3448+                  height: 120965+                  time: 1304133543+                  block_index: 120965+                - hash: 0000000000004cbe5edd3762ca610edba5332926e1c9496e4726452943797a8b+                  height: 120964+                  time: 1304132764+                  block_index: 120964+                - hash: 0000000000002dc81281b024cfd4097e386e0ae7b0797b0c13ec5a3d4328f9e5+                  height: 120963+                  time: 1304132750+                  block_index: 120963+                - hash: 00000000000036e40cb69cdb0e44695d06e06fac00953b73ef4f99b777b2b1ba+                  height: 120962+                  time: 1304132626+                  block_index: 120962+                - hash: 00000000000039ade9fc6ac0c4d198fe7ba1079a1d5ff65fd92adea7b4c12383+                  height: 120961+                  time: 1304132499+                  block_index: 120961+                - hash: 0000000000002c920cf7e4406b969ae9c807b5c4f271f490ca3de1b0770836fc+                  height: 120960+                  time: 1304131980+                  block_index: 120960+                - hash: 000000000000a384acb522e4e5935ad2bc31366ecf1f16f1f11023e967ef033d+                  height: 120959+                  time: 1304131540+                  block_index: 120959+                - hash: 00000000000018af296a8d96a27105b528e8ab8905e01f2d7c8d4d12f0b09a7f+                  height: 120958+                  time: 1304131535+                  block_index: 120958+                - hash: 000000000000832d2dfa3edf46fc7eadb2477946dd73b0cfd54f6a93a3fa75e9+                  height: 120957+                  time: 1304129824+                  block_index: 120957+                - hash: 00000000000031ca732091ab864e0ca3d01c382d3270aaffb96c755d14be4a15+                  height: 120956+                  time: 1304129464+                  block_index: 120956+                - hash: 00000000000096954f1ea00788a00fd1263b9699f9baa3b6ffaf5a44b9e6e892+                  height: 120955+                  time: 1304128815+                  block_index: 120955+                - hash: 00000000000066f10dd6f10f890f511eb86f61f15dbdb2945a6ab3b7b894f811+                  height: 120954+                  time: 1304128647+                  block_index: 120954+                - hash: 0000000000001e163570aa00712337aeccd4e4cc6d721d46412bdbab40d4f2f5+                  height: 120953+                  time: 1304128013+                  block_index: 120953+                - hash: 0000000000000e0365819cafabe8dee9c85959f911546382503ce3e108fbe9f3+                  height: 120952+                  time: 1304127626+                  block_index: 120952+                - hash: 00000000000095c349dd239d1b2129d57c3323affd8f0604fd5945b6bb94974c+                  height: 120951+                  time: 1304126785+                  block_index: 120951+                - hash: 00000000000067e5370cbdc09ed3b6d07656f697e0551b11fb983b2cb7878121+                  height: 120950+                  time: 1304126698+                  block_index: 120950+                - hash: 00000000000046bc557910c589a83e6117763ee3aae6a1a83df646e72d37235b+                  height: 120949+                  time: 1304125871+                  block_index: 120949+                - hash: 0000000000000fa04c2783eaefd10bcde5917ef54dd6ca628886711c4b0aa805+                  height: 120948+                  time: 1304125780+                  block_index: 120948+                - hash: 000000000000b50ad67288000882c78a19c6d237520c6975e7b048d331d678b3+                  height: 120947+                  time: 1304125012+                  block_index: 120947+                - hash: 0000000000007425ae5ed2957026812f654b9a431cdac9b349b812b964ff6f1a+                  height: 120946+                  time: 1304124382+                  block_index: 120946+                - hash: 000000000000a2c8f100a28745fe76f8a1e70f753d3301c5dbb50becef0884e2+                  height: 120945+                  time: 1304124257+                  block_index: 120945+                - hash: 000000000000ad8259a2a50e456c5af3cab76906e338a062a245d934585b548b+                  height: 120944+                  time: 1304124200+                  block_index: 120944+                - hash: 0000000000000ba80470c0420793db59ace3d4428c71e84b3ad9d351c5c0dc3a+                  height: 120943+                  time: 1304123958+                  block_index: 120943+                - hash: 00000000000038128f013486d0654d304c6c1934e613336f38879e0fe42dea94+                  height: 120942+                  time: 1304123312+                  block_index: 120942+                - hash: 000000000000676c8fda22d1fa9d4e89361045dbdd5a136a5ff79577e4f1d350+                  height: 120941+                  time: 1304123030+                  block_index: 120941+                - hash: 0000000000001734d7eda67ca7c38acef939e68433fbe04d7c1d1b98c3090752+                  height: 120940+                  time: 1304122790+                  block_index: 120940+                - hash: 000000000000922ffbbb889287edc43e0e7dade424c88c2f0623165a793af23f+                  height: 120939+                  time: 1304122769+                  block_index: 120939+                - hash: 000000000000476e861117a3a9c2fd4de1765ad93e120325749c15b9ecef4ced+                  height: 120938+                  time: 1304122310+                  block_index: 120938++  '/blockchain/block-height/{height}':+    get:+      tags:+        - Blockchain+      operationId: getBinfoBlockHeight+      summary: Blocks at Height+      description: |+        The blocks at a height, with their transactions, as Blockchain.info answers.+      parameters:+        - name: height+          in: path+          required: true+          schema:+            type: integer+            example: 100+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: List of blocks with transaction data.+          content:+            application/json:+              schema:+                type: object+                required:+                  - blocks+                properties:+                  blocks:+                    type: array+                    items:+                      $ref: '#/components/schemas/BinfoBlock'+              example:+                blocks:+                  - hash: 000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a+                    ver: 1+                    prev_block: 00000000cd9b12643e6854cb25939b39cd7a1ad0af31a9bd8b2efe67854b1995+                    mrkl_root: 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866+                    time: 1231660825+                    bits: 486604799+                    next_block:+                      - 00000000b69bd8e4dc60580117617a466d5c76ada85fb7b87e9baea01f9d9984+                    fee: 0+                    nonce: 1573057331+                    n_tx: 1+                    size: 215+                    block_index: 100+                    main_chain: true+                    height: 100+                    weight: 0+                    tx:+                      - hash: 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866+                        ver: 1+                        vin_sz: 1+                        vout_sz: 1+                        size: 134+                        weight: 536+                        fee: 0+                        relayed_by: 0.0.0.0+                        lock_time: 0+                        tx_index: 3589906961130271+                        double_spend: false+                        time: 1231660825+                        block_index: 100+                        block_height: 100+                        inputs:+                          - sequence: 4294967295+                            witness: ""+                            script: 04ffff001d014d+                            index: 0+                            prev_out: null+                        out:+                          - type: 0+                            spent: false+                            value: 5000000000+                            spending_outpoints: []+                            n: 0+                            tx_index: 3589906961130271+                            script: "4104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff999\+                                    5fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d\+                                    8a93ac"+                            addr: bitcoincash:qqt6g6wul02yakpt05amm0hey67lhh7wagrrqxcmys++  '/blockchain/rawtx/{txid}':+    get:+      tags:+        - Blockchain+      operationId: getBinfoRawTx+      summary: Transaction Data+      description: |+        A transaction by id or by short number, as Blockchain.info answers. With+        `format=hex`, the transaction serialized as in the peer-to-peer protocol.++        A short number that matches several transactions gives 409 with their ids.+      parameters:+        - $ref: '#/components/parameters/binfoTxId'+        - $ref: '#/components/parameters/binfoFormat'+        - $ref: '#/components/parameters/binfoTxIdIndex'+      responses:+        '200':+          description: Transaction data.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/BinfoTx'+              example:+                hash: 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866+                ver: 1+                vin_sz: 1+                vout_sz: 1+                size: 134+                weight: 536+                fee: 0+                relayed_by: 0.0.0.0+                lock_time: 0+                tx_index: 3589906961130271+                double_spend: false+                time: 1231660825+                block_index: 100+                block_height: 100+                inputs:+                  - sequence: 4294967295+                    witness: ""+                    script: 04ffff001d014d+                    index: 0+                    prev_out: null+                out:+                  - type: 0+                    spent: false+                    value: 5000000000+                    spending_outpoints: []+                    n: 0+                    tx_index: 3589906961130271+                    script: "4104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff999\+                            5fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d\+                            8a93ac"+                    addr: bitcoincash:qqt6g6wul02yakpt05amm0hey67lhh7wagrrqxcmys+            application/text:+              schema:+                type: string+                example: "0100000001000000000000000000000000000000000000000000000000000000\+                         0000000000ffffffff0704ffff001d014dffffffff0100f2052a010000004341\+                         04e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995f\+                         c5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a\+                         93ac00000000"+        '404':+          description: Transaction not found.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+        '409':+          description: Transaction index conflict.+          content:+            applicaton/json:+              schema:+                $ref: '#/components/schemas/Error'+              example:+                error: multiple-tx-index+                message: Multiple txs match that index++  '/blockchain/unspent':+    get:+      tags:+        - Blockchain+      operationId: getBinfoUnspent+      summary: Unspent Outputs+      description: |+        The unspent outputs of addresses and xpubs, newest first, as Blockchain.info+        answers. At most 1,000 per call, 250 by default.+      parameters:+        - $ref: '#/components/parameters/binfoActive'+        - $ref: '#/components/parameters/binfoActiveP2SH'+        - $ref: '#/components/parameters/binfoActiveBech32'+        - $ref: '#/components/parameters/binfoLimit'+        - $ref: '#/components/parameters/binfoConfirmations'+        - $ref: '#/components/parameters/binfoTxIdIndex'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Unspent outputs.+          content:+            application/json:+              schema:+                type: object+                required:+                  - unspent_outputs+                properties:+                  unspent_outputs:+                    type: array+                    items:+                      $ref: '#/components/schemas/BinfoUnspent'+                  notice:+                    type: string+              example:+                notice: ""+                unspent_outputs:+                  - tx_hash_big_endian: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    tx_hash: 034192c4695931a90510a7c7c1b528a860b9136ff79e738ec85138fa44a0fff2+                    tx_output_n: 0+                    script: 76a914a367b88dd29119c89cc23130b7ca101e3f0c30df88ac+                    value: 30000+                    value_hex: "7530"+                    confirmations: 8557+                    tx_index: 114565443300134+                    xpub:+                      m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                         ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      path: M/0/73+                  - tx_hash_big_endian: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    tx_hash: 034192c4695931a90510a7c7c1b528a860b9136ff79e738ec85138fa44a0fff2+                    tx_output_n: 1+                    script: 76a91452c5ed64626f1dd39f2d8983a0d94b436524cf8188ac+                    value: 39325+                    value_hex: 00999d+                    confirmations: 8557+                    tx_index: 114565443300134+                    xpub:+                      m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                         ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      path: M/1/27+                  - tx_hash_big_endian: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                    tx_hash: 31b91644e8cdd544b36f9b29606582d91730e4ed65f8a65ff46402a865c43fae+                    tx_output_n: 0+                    script: 76a9145151d153e1e83ee8fd95799c21fbe245c2dedb9588ac+                    value: 15669+                    value_hex: 3d35+                    confirmations: 9853+                    tx_index: 1749472394418618+                    xpub:+                      m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                         ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      path: M/1/26+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+    post:+      tags:+        - Blockchain+      operationId: postBinfoUnspent+      summary: Unspent Outputs+      description: |+        Like the `GET`, with the parameters in a form in the body.+      requestBody:+        content:+          application/x-www-form-urlencoded:+            schema:+              $ref: '#/components/schemas/BinfoUnspentReq'+      responses:+        '200':+          description: Unspent outputs.+          content:+            application/json:+              schema:+                type: object+                required:+                  - unspent_outputs+                properties:+                  unspent_outputs:+                    type: array+                    items:+                      $ref: '#/components/schemas/BinfoUnspent'+                  notice:+                    type: string+              example:+                notice: ""+                unspent_outputs:+                  - tx_hash_big_endian: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    tx_hash: 034192c4695931a90510a7c7c1b528a860b9136ff79e738ec85138fa44a0fff2+                    tx_output_n: 0+                    script: 76a914a367b88dd29119c89cc23130b7ca101e3f0c30df88ac+                    value: 30000+                    value_hex: "7530"+                    confirmations: 8557+                    tx_index: 114565443300134+                    xpub:+                      m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                         ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      path: M/0/73+                  - tx_hash_big_endian: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                    tx_hash: 034192c4695931a90510a7c7c1b528a860b9136ff79e738ec85138fa44a0fff2+                    tx_output_n: 1+                    script: 76a91452c5ed64626f1dd39f2d8983a0d94b436524cf8188ac+                    value: 39325+                    value_hex: 00999d+                    confirmations: 8557+                    tx_index: 114565443300134+                    xpub:+                      m: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                         ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+                      path: M/1/27+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/export-history':+    get:+      tags:+        - Blockchain+      operationId: getBinfoHistory+      summary: Transaction History+      description: |+        The history of a wallet between two dates, one entry per transaction, with+        its value in a currency at the time and now, for accounting.++        Prices come from the price service that the operator configured+        (Blockchain.info by default). If it cannot be reached, the prices are zero+        and the rest of the history is still there.+      parameters:+        - $ref: '#/components/parameters/binfoActive'+        - $ref: '#/components/parameters/binfoActiveP2SH'+        - $ref: '#/components/parameters/binfoActiveBech32'+        - $ref: '#/components/parameters/binfoStart'+        - $ref: '#/components/parameters/binfoEnd'+        - $ref: '#/components/parameters/binfoCurrency'+        - $ref: '#/components/parameters/binfoJsonFormat'+      responses:+        '200':+          description: Transaction history with price.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BinfoHistory'+              example:+                - tx: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                  amount: -6.75e-6+                  time: 10:57:46 GMT +00:00+                  exchange_rate_then: 404.5806367+                  date: "2021-03-25"+                  value_now: -3.86586e-3+                  type: sent+                  value_then: -2.730919297725e-3+                  fee: 6.75e-06+                - tx: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                  amount: -1.998e-5+                  time: 11:27:24 GMT +00:00+                  exchange_rate_then: 440.3835254+                  date: "2021-03-16"+                  value_now: -1.14429456e-2+                  type: sent+                  value_then: -8.798862837492e-3+                  fee: 1.998e-05+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+    post:+      tags:+        - Blockchain+      operationId: postBinfoHistory+      summary: Transaction History+      description: |+        Like the `GET`, with the parameters in a form in the body.+      requestBody:+        content:+          application/x-www-form-urlencoded:+            schema:+              $ref: '#/components/schemas/BinfoHistoryReq'+      responses:+        '200':+          description: Transaction history with price.+          content:+            application/json:+              schema:+                type: array+                items:+                  $ref: '#/components/schemas/BinfoHistory'+              example:+                - tx: f2ffa044fa3851c88e739ef76f13b960a828b5c1c7a71005a9315969c4924103+                  amount: -6.75e-6+                  time: 10:57:46 GMT +00:00+                  exchange_rate_then: 404.5806367+                  date: "2021-03-25"+                  value_now: -3.86586e-3+                  type: sent+                  value_then: -2.730919297725e-3+                  fee: 6.75e-06+                - tx: ae3fc465a80264f45fa6f865ede43017d9826560299b6fb344d5cde84416b931+                  amount: -1.998e-5+                  time: 11:27:24 GMT +00:00+                  exchange_rate_then: 440.3835254+                  date: "2021-03-16"+                  value_now: -1.14429456e-2+                  type: sent+                  value_then: -8.798862837492e-3+                  fee: 1.998e-05+        '404':+          description: Invalid argument.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/addresstohash/{address}':+    get:+      tags:+        - Blockchain+      operationId: getAddressHash+      summary: Address Hash+      description: |+        The hash160 of an address, in hexadecimal, as plain text.++        The `/blockchain/q/` endpoints answer in plain text rather than JSON, as+        those of Blockchain.info do, so that a shell script can use the answer as it+        is.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Address hash.+          content:+            text/plain:+              schema:+                type: string+                example: 37d070b3c6716a6174080722579fbdddad39bfb3+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/hashtoaddress/{hash}':+    get:+      tags:+        - Blockchain+      operationId: getHashAddress+      summary: Hash Address+      description: |+        The pay-to-public-key-hash address of a hash160, as plain text.+      parameters:+        - $ref: '#/components/parameters/hashParam'+      responses:+        '200':+          description: Hash address.+          content:+            text/plain:+              schema:+                type: string+                example: qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+        '404':+          description: Invalid hash.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/addrpubkey/{pubkey}':+    get:+      tags:+        - Blockchain+      operationId: getPubKeyAddress+      summary: Public Key Address+      description: |+        The pay-to-public-key-hash address of a public key, as plain text.+      parameters:+        - $ref: '#/components/parameters/pubKeyParam'+      responses:+        '200':+          description: Public key address.+          content:+            text/plain:+              schema:+                type: string+                example: bitcoincash:qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+        '404':+          description: Invalid public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/pubkeyaddr/{address}':+    get:+      tags:+        - Blockchain+      operationId: getAddressPubKey+      summary: Address Public Key+      description: |+        The public key of an address, in hexadecimal, as plain text.++        An address is a hash of its public key, so the key can only be found once the+        address has spent something: the server looks for an input that spends from+        the address among its recent transactions, and reads the key from it. An+        address that has never spent gives 404.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Address public key.+          content:+            text/plain:+              schema:+                type: string+                example: 027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d4c07b9f8c3d04327794c+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/hashpubkey/{pubkey}':+    get:+      tags:+        - Blockchain+      operationId: getPubKeyHash+      summary: Public Key Hash+      description: |+        The hash160 of a public key, in hexadecimal, as plain text.+      parameters:+        - $ref: '#/components/parameters/pubKeyParam'+      responses:+        '200':+          description: Public key hash.+          content:+            text/plain:+              schema:+                type: string+                example: 37d070b3c6716a6174080722579fbdddad39bfb3+        '404':+          description: Invalid public key.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/getblockcount':+    get:+      tags:+        - Blockchain+      operationId: getBlockCount+      summary: Block Count+      description: |+        The height of the newest block header, as plain text. Headers arrive before+        blocks, so it can be ahead of `/block/best`.+      responses:+        '200':+          description: Height of block chain.+          content:+            text/plain:+              schema:+                type: number+                example: 688824++  '/blockchain/q/latesthash':+    get:+      tags:+        - Blockchain+      operationId: getLatestHash+      summary: Latest Block Hash+      description: |+        The hash of the newest block header, as plain text.+      responses:+        '200':+          description: Latest block hash.+          content:+            text/plain:+              schema:+                type: string+                example: 0000000000000000005d55b8ab4de4c864b815edf9fb52278d69a614c3c7c72a++  '/blockchain/q/bcperblock':+    get:+      tags:+        - Blockchain+      operationId: getBlockSubsidy+      summary: Block Subsidy+      description: |+        The subsidy of the next block, in satoshis, as plain text: the new coins its+        miner will create, which halve every 210,000 blocks.+      responses:+        '200':+          description: Block subsidy for next block.+          content:+            text/plain:+              schema:+                type: number+                example: 6.25++  '/blockchain/q/txtotalbtcoutput/{txid}':+    get:+      tags:+        - Blockchain+      operationId: getTotalOutput+      summary: Total Output+      description: |+        The total value of the outputs of a transaction, in satoshis, as plain text.+      parameters:+        - $ref: '#/components/parameters/binfoTxId'+      responses:+        '200':+          description: Total amount in transaction outputs.+          content:+            text/plain:+              schema:+                type: number+                example: 9.94e-4+        '404':+          description: Invalid transaction.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/txtotalbtcinput/{txid}':+    get:+      tags:+        - Blockchain+      operationId: getTotalInput+      summary: Total Spent+      description: |+        The total value of the outputs that a transaction spends, in satoshis, as+        plain text.+      parameters:+        - $ref: '#/components/parameters/binfoTxId'+      responses:+        '200':+          description: Total amount spent by transaction.+          content:+            text/plain:+              schema:+                type: number+                example: 9.9973e-4+        '404':+          description: Invalid transaction.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/txfee/{txid}':+    get:+      tags:+        - Blockchain+      operationId: getTxFee+      summary: Transaction Fee+      description: |+        The fee of a transaction, in satoshis, as plain text: what it spends minus+        what it pays out.+      parameters:+        - $ref: '#/components/parameters/binfoTxId'+      responses:+        '200':+          description: Fees paid by transaction.+          content:+            text/plain:+              schema:+                type: number+                example: 5.73e-6+        '404':+          description: Invalid transaction.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/txresult/{txid}/{address}':+    get:+      tags:+        - Blockchain+      operationId: getTxResult+      summary: Transaction Address Amount+      description: |+        How much a transaction changes the balance of an address, in satoshis, as+        plain text: what it pays to the address minus what it spends from it.+      parameters:+        - $ref: '#/components/parameters/addressParam'+        - $ref: '#/components/parameters/binfoTxId'+      responses:+        '200':+          description: Value of transaction affecting address balance.+          content:+            text/plain:+              schema:+                type: number+                example: 0+        '404':+          description: Invalid transaction or address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/getreceivedbyaddress/{address}':+    get:+      tags:+        - Blockchain+      operationId: getReceivedByAddress+      summary: Total Received by Address+      description: |+        The total ever received by an address, in satoshis, as plain text.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Value of total received by address+          content:+            text/plain:+              schema:+                type: number+                example: 2275183814+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/getsentbyaddress/{address}':+    get:+      tags:+        - Blockchain+      operationId: getSentByAddress+      summary: Total Sent by Address+      description: |+        The total ever sent from an address, in satoshis, as plain text.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Value of total sent by address+          content:+            text/plain:+              schema:+                type: number+                example: 2275183814+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/addressbalance/{address}':+    get:+      tags:+        - Blockchain+      operationId: getAddressBalance+      summary: Balance for Address+      description: |+        The balance of an address, confirmed and unconfirmed, in satoshis, as plain+        text.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Total unspent balance for address.+          content:+            text/plain:+              schema:+                type: number+                example: 0+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'++  '/blockchain/q/addressfirstseen/{address}':+    get:+      tags:+        - Blockchain+      operationId: getAddressFirstSeen+      summary: Address First Seen+      description: |+        The time of the first block with a transaction of an address, as a Unix+        timestamp in plain text, or `0` if it has none.+      parameters:+        - $ref: '#/components/parameters/addressParam'+      responses:+        '200':+          description: Time when address was first seen.+          content:+            text/plain:+              schema:+                type: number+                example: 1621533785+        '404':+          description: Invalid address.+          content:+            application/json:+              schema:+                $ref: '#/components/schemas/Error'+++  '/dbstats':+    get:+      tags:+        - Network+      summary: Database Statistics+      operationId: getDbStats+      description: |+        How the database of this server uses memory, by column family, and the+        statistics that RocksDB keeps, as plain text for the operator to read. The+        format is that of RocksDB and may change between versions, so do not parse+        it.+      responses:+        '200':+          description: Memory use and statistics of RocksDB.+          content:+            text/plain:+              schema:+                type: string++components:+  schemas:++    BlockRef:+      description: |+        Where a transaction or output is: in a block, with its height and position,+        or in the mempool, with the time this server first saw it. A transaction+        moves from one form to the other when it is confirmed, or back after a+        reorganisation.+      oneOf:+        - $ref: '#/components/schemas/Block'+        - $ref: '#/components/schemas/Mempool'++    Block:+      type: object+      description: |+        A place in a block.+      required:+        - height+        - position+      properties:+        height:+          type: integer+          description: |+            Height of the block.+        position:+          type: integer+          description: |+            Position of the transaction in the block, from 0.++    Mempool:+      type: object+      description: |+        A place in the mempool.+      required:+        - mempool+      properties:+        mempool:+          type: integer+          description: |+            Unix time when this server first saw the transaction. Other servers may+            have seen it at another time; it orders the unconfirmed transactions.+          format: int64++    XPubBalance:+      type: object+      description: |+        The balance of one address of a wallet, with its derivation path below the+        extended public key.+      required:+        - path+        - balance+      properties:+        path:+          $ref: '#/components/schemas/XPubPath'+        balance:+          $ref: '#/components/schemas/Balance'++    XPubPath:+      type: array+      description: |+        Derivation path of an address below the extended public key: the chain+        (`0` receiving, `1` change) and the index of the address in it, so `[1, 4]`+        is the fifth change address.+      items:+        type: integer++    XPubUnspent:+      type: object+      description: |+        An unspent output of a wallet, with the derivation path of its address, so+        the wallet knows which key signs for it.+      required:+        - path+        - unspent+      properties:+        path:+          $ref: '#/components/schemas/XPubPath'+        unspent:+          $ref: '#/components/schemas/Unspent'++    XPubSummary:+      type: object+      description: |+        The balance of a wallet and how far it has used its addresses.+      required:+        - balance+        - indices+      properties:+        balance:+          type: object+          description: |+            Totals over every address of the wallet, in satoshis: `confirmed`,+            `unconfirmed` and `received` as in an address balance, and `utxo`, the+            number of unspent outputs.+          required:+            - confirmed+            - unconfirmed+            - received+            - utxo+          properties:+            confirmed:+              description: Confirmed balance for all addresses.+              type: integer+            unconfirmed:+              description: Unconfirmed balance for all addresses.+              type: integer+            received:+              description: Total received on all external addresses.+              type: integer+            utxo:+              description: Unspent transaction output count.+              type: integer+        indices:+          type: object+          description: |+            Index of the last used address of the receiving chain (`external`) and of+            the change chain (`change`). The next unused address is one past it.+          required:+            - external+            - change+          properties:+            external:+              type: integer+            change:+              type: integer++    TxRef:+      type: object+      description: |+        A transaction id and where the transaction is. Lists of transactions return+        these rather than full transactions, which are much larger; fetch the ones+        you need.+      required:+        - block+        - txid+      properties:+        block:+          $ref: '#/components/schemas/BlockRef'+        txid:+          type: string+          description: |+            Transaction id.++    Balance:+      type: object+      description: |+        The balance of an address. `confirmed` plus `unconfirmed` is what the address+        holds now, counting unconfirmed transactions; `confirmed` alone is what it+        holds in coins that are in blocks and are not being spent.+      required:+        - address+        - confirmed+        - unconfirmed+        - utxo+        - received+        - txs+      properties:+        address:+          type: string+          description: |+            The address.+        confirmed:+          type: integer+          description: |+            Sum of the unspent outputs of the address that are confirmed, in satoshis.+            An output spent by an unconfirmed transaction no longer counts.+          format: int64+        unconfirmed:+          type: integer+          description: |+            Sum of the unspent outputs of the address that are not confirmed yet, in+            satoshis.+          format: int64+        utxo:+          type: integer+          description: |+            Number of unspent outputs.+          format: int64+        received:+          type: integer+          description: |+            Total ever paid to the address, spent or not, confirmed or not, in+            satoshis.+          format: int64+        txs:+          type: integer+          description: |+            Number of transactions that pay to or spend from the address.+          format: int64++    Unspent:+      type: object+      description: |+        An output that is not spent yet: a coin that can be spent.+      required:+        - address+        - block+        - txid+        - index+        - pkscript+        - value+      properties:+        address:+          type: string+          description: |+            Address of the output. Null for scripts that have no address, such as+            data outputs or bare multisig.+          nullable: true+        block:+          $ref: '#/components/schemas/BlockRef'+        txid:+          type: string+          description: |+            Id of the transaction that created the output.+        index:+          type: integer+          description: |+            Position of the output in that transaction, from 0.+        pkscript:+          type: string+          description: |+            Output script in hexadecimal, needed to sign a spend.+        value:+          type: integer+          description: |+            Value in satoshis.+          format: int64++    BlockData:+      type: object+      description: |+        A block, with its header and what the server computed from it.+      required:+        - hash+        - height+        - mainchain+        - previous+        - time+        - version+        - bits+        - nonce+        - size+        - tx+        - merkle+        - subsidy+        - fees+        - outputs+        - work+      properties:+        hash:+          type: string+          description: |+            Block hash.+        size:+          type: integer+          description: |+            Size of the serialized block, in bytes, witness data included.+        height:+          type: integer+          description: |+            Height in its chain.+        mainchain:+          type: boolean+          description: |+            Whether the block is in the chain with the most work. Blocks of chains+            abandoned in a reorganisation are kept, with `false`.+        previous:+          type: string+          description: |+            Hash of the previous block.+        time:+          type: integer+          description: |+            Timestamp set by the miner, as a Unix time. Only roughly right.+          format: int64+        version:+          type: integer+          description: |+            Block version.+          format: int64+        bits:+          type: integer+          description: |+            Proof-of-work target, in compact form.+          format: int64+        nonce:+          type: integer+          description: |+            Nonce found by the miner.+          format: int64+        tx:+          type: array+          description: |+            Ids of the transactions, in order; the first is the coinbase.+          items:+            type: string+        merkle:+          type: string+          description: |+            Merkle root of the transactions.+        subsidy:+          type: integer+          description: |+            New coins created by the block, in satoshis.+          format: int64+        fees:+          type: integer+          description: |+            Sum of the fees of its transactions, in satoshis.+          format: int64+        outputs:+          type: integer+          description: |+            Sum of the outputs of its transactions, in satoshis.+          format: int64+        weight:+          type: integer+          description: |+            Block weight, on networks with SegWit only.+        work:+          type: integer+          description: |+            Proof of work accumulated by the chain up to this block.++    Input:+      type: object+      description: |+        An input of a transaction, with the address and value of the output it+        spends, which the raw transaction does not contain and which the server+        looks up.+      required:+        - coinbase+        - txid+        - output+        - sigscript+        - sequence+        - pkscript+        - witness+        - value+        - address+      properties:+        coinbase:+          type: boolean+          description: |+            Whether this is the input of a coinbase, which spends nothing.+        txid:+          type: string+          description: |+            Id of the transaction whose output is spent.+        output:+          type: integer+          description: |+            Position of that output in its transaction.+        sigscript:+          type: string+          description: |+            Input script in hexadecimal.+        value:+          type: integer+          description: |+            Value of the output spent, in satoshis.+          format: int64+          nullable: true+        sequence:+          type: integer+          description: |+            Sequence number.+          format: int64+        address:+          type: string+          description: |+            Address of the output spent; null if it has none.+          nullable: true+        pkscript:+          type: string+          description: |+            Script of the output spent, in hexadecimal.+          nullable: true+        witness:+          type: array+          description: |+            Witness stack, each item in hexadecimal; empty without SegWit.+          items:+            type: string++    Spender:+      type: object+      description: |+        The input that spends an output.+      required:+        - txid+        - input+      properties:+        txid:+          type: string+          description: |+            Id of the spending transaction.+        input:+          type: integer+          description: |+            Position of the input in that transaction.++    Output:+      type: object+      description: |+        An output of a transaction, with whether and by what it is spent.+      required:+        - address+        - pkscript+        - value+        - spent+      properties:+        address:+          type: string+          description: |+            Address of the output; null for scripts without one.+          nullable: true+        pkscript:+          type: string+          description: |+            Output script in hexadecimal.+        value:+          type: integer+          description: |+            Value in satoshis.+          format: int64+        spent:+          type: boolean+          description: |+            Whether a transaction spends it, confirmed or not.+        spender:+          description: |+            The input that spends it, if any.+          allOf:+            - $ref: '#/components/schemas/Spender'++    Transaction:+      type: object+      description: |+        A transaction with what the server knows about it beyond its raw form: its+        place in the chain, its fee, and, for each input and output, the address and+        value involved.+      required:+        - txid+        - size+        - version+        - locktime+        - fee+        - inputs+        - outputs+        - block+        - deleted+        - time+        - rbf+        - weight+      properties:+        txid:+          type: string+          description: |+            Transaction id.+        size:+          type: integer+          description: |+            Size of the serialized transaction, in bytes, witness data included.+        version:+          type: integer+          description: |+            Transaction version.+          format: int64+        locktime:+          type: integer+          description: |+            Lock time.+          format: int64+        fee:+          type: integer+          description: |+            Fee in satoshis: inputs minus outputs; 0 for a coinbase.+          format: int64+        inputs:+          type: array+          items:+            $ref: '#/components/schemas/Input'+        outputs:+          type: array+          items:+            $ref: '#/components/schemas/Output'+        block:+          $ref: '#/components/schemas/BlockRef'+        deleted:+          type: boolean+          description: |+            Always false. The server no longer returns transactions that were+            removed: replaced by fee, double-spent by a confirmed transaction, or+            the coinbase of a block that left the main chain. The field is kept so+            that clients that read it go on working.+        time:+          type: integer+          description: |+            Timestamp of its block, or, if unconfirmed, the Unix time this server first+            saw it; in a fleet, the time at which the first server of the fleet did. A+            confirmed transaction has the time of its block on every server, whether+            the server saw it before the block or not: until database version 22 a+            server that had seen it unconfirmed went on giving the time of that. A+            transaction that goes back to the mempool because its block left the main+            chain keeps the time of that block.+          format: int64+        rbf:+          type: boolean+          description: |+            Whether the transaction can still be replaced by one paying a higher fee:+            it signals replace-by-fee (BIP 125), or spends an unconfirmed transaction+            that does. Only meaningful while unconfirmed, and on BTC.+        weight:+          type: integer+          description: |+            Transaction weight, as SegWit counts it.++    Event:+      type: object+      description: |+        Something that happened, as `/events` sends it.+      required:+        - type+        - id+      properties:+        type:+          type: string+          description: |+            `block` for a new best block, `tx` for a mempool change.+          enum:+            - tx+            - block+        id:+          type: string+          description: |+            Hash of the block or id of the transaction.++    Peer:+      type: object+      description: |+        A node of the peer-to-peer network that the server is connected to.+      required:+        - useragent+        - address+        - version+        - services+        - relay+      properties:+        useragent:+          type: string+          description: |+            Software of the peer, as it announces itself.+        address:+          type: string+          description: |+            Network address of the peer.+        version:+          type: integer+          description: |+            Protocol version from the version message of the peer.+          format: int32+        services:+          type: string+          description: |+            Services the peer offers, as a bit field in hexadecimal.+        relay:+          type: boolean+          description: |+            Whether the peer relays transactions.++    BlockHealth:+      type: object+      description: |+        Whether the blocks are caught up with the headers.+      required:+        - headers+        - blocks+        - diff+        - max+        - ok+      properties:+        headers:+          type: integer+          description: |+            Height of the newest header.+        blocks:+          type: integer+          description: |+            Height of the newest imported block.+        diff:+          type: integer+          description: |+            Headers minus blocks.+        max:+          type: integer+          description: |+            Most blocks the server may be behind and still be healthy.+        ok:+          type: boolean+          description: |+            Whether `diff` is at most `max`.++    TimeHealth:+      type: object+      description: |+        Whether something happened recently enough.+      required:+        - age+        - max+        - ok+      properties:+        age:+          type: integer+          description: |+            Seconds since it happened.+        max:+          type: integer+          description: |+            Most seconds allowed.+        ok:+          type: boolean+          description: |+            Whether `age` is at most `max`.++    CountHealth:+      type: object+      description: |+        Whether there are enough of something.+      required:+        - count+        - min+        - ok+      properties:+        count:+          type: integer+          description: |+            How many there are.+        min:+          type: integer+          description: |+            How many are needed.+        ok:+          type: boolean+          description: |+            Whether `count` is at least `min`.++    MaxHealth:+      type: object+      description: |+        Whether there are not too many of something.+      required:+        - count+        - max+        - ok+      properties:+        count:+          type: integer+          description: |+            How many there are.+        max:+          type: integer+          description: |+            Most allowed.+        ok:+          type: boolean+          description: |+            Whether `count` is at most `max`.++    HealthCheck:+      type: object+      description: |+        The result of the latest health check. `ok` is true when every check passes.+        The limits are set by the operator of the server.+      required:+        - blocks+        - last-block+        - last-tx+        - pending-txs+        - peers+        - net+        - version+        - ok+      properties:+        blocks:+          description: |+            Whether the imported blocks have caught up with the headers.+          allOf:+            - $ref: '#/components/schemas/BlockHealth'+        last-block:+          description: |+            Age of the newest header. A network that has found no block for hours+            usually means the server lost touch with it.+          allOf:+            - $ref: '#/components/schemas/TimeHealth'+        last-tx:+          description: |+            Age of the newest unconfirmed transaction (or block, if newer). A busy+            network sends transactions all the time, so silence means trouble.+          allOf:+            - $ref: '#/components/schemas/TimeHealth'+        pending-txs:+          description: |+            Transactions received but not imported yet.+          allOf:+            - $ref: '#/components/schemas/MaxHealth'+        peers:+          description: |+            Connected peers.+          allOf:+            - $ref: '#/components/schemas/CountHealth'+        redis:+          type: object+          description: |+            Whether the Redis cache answers. Only for a server that has one. It does+            not count towards `ok`: a server without its Redis still answers every+            request, from its own database, and `/health` gives 203.+          required:+            - ok+          properties:+            ok:+              type: boolean+        net:+          type: string+          description: |+            Network of the server.+        version:+          type: string+          description: |+            Version of Haskoin Store.+        ok:+          type: boolean+          description: |+            Whether every check passed.+        time:+          type: string+          description: |+            When the check ran.++    Error:+      type: object+      description: |+        An error. `error` is a stable code for programs to test; `message` is for+        people and may change. See *Errors* at the top.+      required:+        - error+      properties:+        error:+          type: string+          description: |+            Error code, such as `not-found-or-invalid-arg` or `user-error`.+          example: not-found-or-invalid-arg+        message:+          type: string+          description: |+            What went wrong, for people.+          example: Item not found or argument invalid+    TxId:+      type: object+      description: |+        The id of a transaction.+      required:+        - txid+      properties:+        txid:+          type: string+          description: |+            Transaction id.++    BinfoTxIdHash:+      type: string++    BinfoTxIdIndex:+      type: integer++    BinfoTxId:+      description: |+        A transaction id in hexadecimal, or its short number (see the `txidindex`+        parameter).+      oneOf:+        - $ref: '#/components/schemas/BinfoTxIdHash'+        - $ref: '#/components/schemas/BinfoTxIdIndex'+      example: 2d05f0c9c3e1c226e63b5fac240137687544cf631cd616fd34fd188fc9020866++    BinfoWallet:+      type: object+      required:+        - final_balance+        - n_tx+        - n_tx_filtered+        - total_received+        - total_sent+      properties:+        final_balance:+          type: integer+        n_tx:+          type: integer+          description: |+            Number of transactions of the addresses and extended public keys+            shown (`onlyShow`, or all the active ones), each counted once, not+            only those on this page.+        n_tx_filtered:+          type: integer+          description: |+            Number of those transactions that pass the `filter`, not only+            those on this page.+        total_received:+          type: integer+        total_sent:+          type: integer+++    BinfoSymbol:+      type: object+      required:+        - code+        - symbol+        - name+        - conversion+        - symbolAppearsAfter+        - local+      properties:+        code:+          type: string+        symbol:+          type: string+        name:+          type: string+        conversion:+          type: number+          format: double+        symbolAppearsAfter:+          type: boolean+        local:+          type: boolean++    BinfoBlockInfo:+      type: object+      required:+        - hash+        - height+        - time+        - block_index+      properties:+        hash:+          type: string+        height:+          type: integer+        time:+          type: integer+        block_index:+          type: integer++    BinfoInfo:+      type: object+      required:+        - nconnected+        - conversion+        - symbol_local+        - symbol_btc+        - latest_block+      properties:+        nconnected:+          type: integer+        conversion:+          type: number+          format: double+        symbol_local:+          $ref: '#/components/schemas/BinfoSymbol'+        symbol_btc:+          $ref: '#/components/schemas/BinfoSymbol'+        latest_block:+          $ref: '#/components/schemas/BinfoBlockInfo'++    BinfoXPubBalance:+      type: object+      required:+        - address+        - final_balance+        - n_tx+        - total_received+        - total_sent+        - account_index+        - change_index+      properties:+        address:+          type: string+        final_balance:+          type: integer+          format: int64+        n_tx:+          type: integer+        total_received:+          type: integer+          format: int64+        total_sent:+          type: integer+          format: int64+        account_index:+          type: integer+        change_index:+          type: integer+++    BinfoAddrBalance:+      type: object+      required:+        - address+        - final_balance+        - n_tx+        - total_received+        - total_sent+      properties:+        address:+          type: string+        final_balance:+          type: integer+          format: int64+        n_tx:+          type: integer+        total_received:+          type: integer+          format: int64+        total_sent:+          type: integer+          format: int64++    BinfoRawAddr:+      type: object+      required:+        - hash160+        - address+        - n_tx+        - n_unredeemed+        - total_received+        - total_sent+        - final_balance+        - txs+      properties:+        hash160:+          type: string+        address:+          type: string+        n_tx:+          type: integer+          description: |+            Number of transactions of the address or extended public key, not+            only those on this page.+        n_unredeemed:+          type: integer+        total_received:+          type: integer+          format: int64+        total_sent:+          type: integer+          format: int64+        final_balance:+          type: integer+          format: int64+        txs:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTx'++    BinfoBalance:+      oneOf:+        - $ref: '#/components/schemas/BinfoXPubBalance'+        - $ref: '#/components/schemas/BinfoAddrBalance'++    BinfoShortBalance:+      type: object+      required:+        - final_balance+        - n_tx+        - total_received+      properties:+        final_balance:+          type: integer+          format: int64+        n_tx:+          type: integer+        total_received:+          type: integer+          format: int64++    BinfoMultiAddr:+      type: object+      required:+        - wallet+        - info+        - addresses+        - recommend_include_fee+      properties:+        wallet:+          $ref: '#/components/schemas/BinfoWallet'+        info:+          $ref: '#/components/schemas/BinfoInfo'+        addresses:+          type: array+          items:+            $ref: '#/components/schemas/BinfoBalance'+        txs:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTx'+        recommend_include_fee:+          type: boolean+        cash_addr:+          type: boolean++    BinfoTxInput:+      type: object+      required:+        - index+        - script+        - prev_out+        - sequence+        - witness+      properties:+        index:+          type: integer+        script:+          type: string+        prev_out:+          $ref: '#/components/schemas/BinfoTxOutput'+        sequence:+          type: integer+        witness:+          type: string++    BinfoXPubPath:+      type: object+      required:+        - 'm'+        - path+      properties:+        m:+          type: string+        path:+          type: string++    BinfoSpender:+      type: object+      required:+        - tx_index+        - 'n'+      properties:+        tx_index:+          $ref: '#/components/schemas/BinfoTxId'+        n:+          type: integer++    BinfoTxOutput:+      type: object+      required:+        - tx_index+        - 'n'+        - type+        - spent+        - value+        - spending_outpoints+        - script+      properties:+        type:+          type: integer+        spent:+          type: boolean+        value:+          type: integer+          format: int64+        spending_outpoints:+          $ref: '#/components/schemas/BinfoSpender'+        n:+          type: integer+        tx_index:+          $ref: '#/components/schemas/BinfoTxId'+        script:+          type: string+        addr:+          type: string+        xpub:+          $ref: '#/components/schemas/BinfoXPubPath'++    BinfoHeader:+      type: object+      required:+        - hash+        - time+        - block_index+        - height+        - txIndexes+      properties:+        hash:+          type: string+        time:+          type: integer+        block_index:+          type: integer+        height:+          type: integer+        txIndexes:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTxId'+++    BinfoBlock:+      type: object+      required:+        - hash+        - ver+        - prev_block+        - mrkl_root+        - time+        - bits+        - next_block+        - fee+        - nonce+        - n_tx+        - size+        - block_index+        - main_chain+        - height+        - weight+        - tx+      properties:+        hash:+          type: string+        ver:+          type: integer+        prev_block:+          type: string+        mrkl_root:+          type: string+        time:+          type: integer+        bits:+          type: integer+        next_block:+          type: array+          items:+            type: string+        fee:+          type: integer+          format: int64+        nonce:+          type: integer+        n_tx:+          type: integer+        size:+          type: integer+        block_index:+          type: integer+        main_chain:+          type: boolean+        height:+          type: integer+        weight:+          type: integer+        tx:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTx'++    BinfoHistory:+      type: object+      required:+        - date+        - time+        - type+        - amount+        - value_then+        - value_now+        - exchange_rate_then+        - tx+        - fee+      properties:+        date:+          type: string+        time:+          type: string+        type:+          type: string+          enum:+            - sent+            - received+        amount:+          type: number+        value_then:+          type: number+        value_now:+          type: number+        exchange_rate_then:+          type: number+        tx:+          type: string+        fee:+          type: number++    BinfoTx:+      type: object+      required:+        - hash+        - ver+        - vin_sz+        - vout_sz+        - size+        - weight+        - fee+        - relayed_by+        - lock_time+        - tx_index+        - double_spend+        - time+        - block_index+        - block_height+        - inputs+        - out+      properties:+        hash:+          type: string+        ver:+          type: integer+        vin_sz:+          type: integer+        vout_sz:+          type: integer+        size:+          type: integer+        weight:+          type: integer+        fee:+          type: integer+          format: int64+        relayed_by:+          type: string+        lock_time:+          type: integer+        tx_index:+          $ref: '#/components/schemas/BinfoTxId'+        double_spend:+          type: boolean+        time:+          type: integer+        block_index:+          type: integer+        block_height:+          type: integer+        inputs:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTxInput'+        out:+          type: array+          items:+            $ref: '#/components/schemas/BinfoTxOutput'+        result:+          type: integer+          format: int64+        balance:+          type: integer+          format: int64+        rbf:+          type: boolean++    BinfoUnspent:+      type: object+      required:+        - tx_hash_big_endian+        - tx_hash+        - tx_output_n+        - script+        - value+        - value_hex+        - confirmations+        - tx_index+      properties:+        tx_hash_big_endian:+          type: string+        tx_hash:+          type: string+        tx_output_n:+          type: integer+        script:+          type: string+        value:+          type: integer+          format: int64+        value_hex:+          type: string+        confirmations:+          type: integer+        tx_index:+          $ref: '#/components/schemas/BinfoTxId'++    BinfoBalanceReq:+      type: object+      properties:+        active:+          type: array+          items:+            type: string+            example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                     ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+        activeP2SH:+          type: array+          items:+            type: string+        activeBech32:+          type: array+          items:+            type: string+        cashaddr:+          type: boolean+          example: true++    BinfoHistoryReq:+      type: object+      properties:+        active:+          type: array+          items:+            type: string+            example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                     ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+        activeP2SH:+          type: array+          items:+            type: string+        activeBech32:+          type: array+          items:+            type: string+        currency:+          type: string+          example: eur+        start:+          type: string+          example: 20-10-2020+        end:+          type: string+          example: 09-04-2021++    BinfoMultiAddrReq:+      type: object+      properties:+        active:+          type: array+          items:+            type: string+            example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                     ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+        activeP2SH:+          type: array+          items:+            type: string+        activeBech32:+          type: array+          items:+            type: string+        onlyShow:+          type: array+          items:+            type: string+        n:+          type: integer+          example: 2+        offset:+          type: integer+          example: 5+        noCompact:+          type: boolean+          example: false+        currency:+          type: string+          example: eur+        cashaddr:+          type: boolean+          example: true+        txidindex:+          type: boolean+          example: true+        filter:+          type: integer+          enum: [0,1,2,3,5,7]+          example: 5++    BinfoUnspentReq:+      type: object+      properties:+        active:+          type: array+          items:+            type: string+            example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                     ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"+        activeP2SH:+          type: array+          items:+            type: string+        activeBech32:+          type: array+          items:+            type: string+        limit:+          type: integer+          example: 2+        confirmations:+          type: integer+          example: 6+        txidindex:+          type: boolean+          example: true++  parameters:++    pubKeyParam:+      name: pubkey+      in: path+      description: |+        A public key in hexadecimal, compressed or not.+      required: true+      schema:+        type: string+        example: 027eb504988cbc043dd4c9fac51685d68718a24d7f9f1d4c07b9f8c3d04327794c++    addressParam:+      name: address+      in: path+      description: |+        An address of the network of the server. An address that does not parse,+        or is of another network, gives 404.+      required: true+      schema:+        type: string+      example: qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u++    hashParam:+      name: hash+      in: path+      description: |+        A hash160 (20 bytes) in hexadecimal.+      required: true+      schema:+        type: string+      example: 37d070b3c6716a6174080722579fbdddad39bfb3++    addressesParam:+      name: addresses+      in: query+      description: |+        Addresses, separated by commas. Every address must be valid: one that is not+        fails the whole request with 400, rather than giving an answer that silently+        leaves it out.+      explode: false+      required: true+      schema:+        type: array+        items:+          type: string+        example:+          - qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u+          - qql2wpu4fa36yf07gcc8q80sjfxfaq66xssezyukg2++    limitParam:+      name: limit+      in: query+      description: |+        How many items to return at most. Without it, the server's default (100+        unless the operator changed it). `0` asks for the server's maximum. A+        larger number is lowered to the maximum rather than refused. See *Paging* at+        the top.+      required: false+      schema:+        type: integer++    heightParam:+      name: height+      in: query+      description: |+        Where the page starts: a transaction id (included), a block hash, a block+        height (transactions confirmed at or below it), or a Unix time (the last+        block at or before it). Numbers up to 1,230,768,000 are heights, larger ones+        times. Unconfirmed transactions come first in every list, and a page that+        starts at a height, a block or a time has none of them, even when the height+        is above the best block.++        To get the next page, pass the id of the last transaction you received here,+        with `offset=1`. See *Paging* at the top for why this is better than a large+        offset.+      required: false+      schema:+        oneOf:+          - type: integer+          - type: string++    offsetParam:+      name: offset+      in: query+      description: |+        How many items to skip after the start. Above the server's maximum (50,000+        unless the operator changed it), the request fails with 400; use `height` to+        go further back.+      required: false+      schema:+        type: integer++    nocacheParam:+      name: nocache+      in: query+      description: |+        Answer from the database of this server, without reading the Redis cache and+        without caching the wallet. The answer is the same; this is for comparing,+        and for servers whose cache is shared with others that may be ahead of or+        behind this one. A server of a fleet still answers from the block and the+        mempool that the fleet shares, only without its cache of wallets.+      required: false+      schema:+        type: boolean++    notxParam:+      name: notx+      in: query+      description: |+        Leave out the transaction ids, except the first (the coinbase). A block can+        have thousands of transactions, and pages that list blocks rarely need them.+      required: false+      schema:+        type: boolean++    blockParam:+      name: block+      in: path+      description: |+        A block hash in hexadecimal.+      required: true+      schema:+        type: string+        example: 000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506++    blocksParam:+      name: blocks+      in: query+      description: |+        Block hashes in hexadecimal, separated by commas.+      explode: false+      required: true+      schema:+        type: array+        items:+          type: string+        example:+          - 000000001aeae195809d120b5d66a39c83eb48792e068f8ea1fea19d84a4278a+          - 000000000845517b31c6820d83f25cff46429bf136a7515fe504116427e60f8e++    blockHeightParam:+      name: height+      in: path+      description: |+        A block height. The first block is at height 0.+      required: true+      schema:+        type: integer+      example: 200++    blockHeightsParam:+      name: heights+      in: query+      description: |+        Block heights, separated by commas.+      explode: false+      required: true+      schema:+        type: array+        items:+          type: integer+        example:+          - 100+          - 200+          - 300++    txidParam:+      name: txid+      in: path+      description: |+        A transaction id in hexadecimal.+      required: true+      schema:+        type: string+        example: 03c16602a41ae7cbc2a25b0d2801fdc3295e2e8ebfb26774030b2f7d538f5568++    timeParam:+      name: time+      in: path+      description: |+        A Unix timestamp, in seconds.+      required: true+      schema:+        type: integer+        example: 1231753120++    txidsParam:+      name: txids+      in: query+      description: |+        Transaction ids in hexadecimal, separated by commas.+      explode: false+      required: true+      schema:+        type: array+        items:+          type: string+        example:+          - 03c16602a41ae7cbc2a25b0d2801fdc3295e2e8ebfb26774030b2f7d538f5568+          - 02ea64dfff60b1f215282bbb4b2d729fecc50d81d2d0ef09a60d550a5a69418f++    xpubParam:+      name: xpub+      in: path+      description: |+        An extended public key of the network of the server: `xpub` on BTC and BCH,+        `tpub` on test networks. Keys that wallets export as `ypub` or `zpub` are+        not accepted; export them as `xpub` and say the kind of address with+        `derive`.+      required: true+      schema:+        type: string+        example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                 ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"++    xpubDerive:+      name: derive+      in: query+      description: |+        The kind of address the wallet derives from the key:++        - `legacy`: pay-to-public-key-hash (P2PKH), addresses starting with `1`+          on BTC; the default, and the only kind on networks without SegWit, such as+          BCH. `normal`, its name up to version 1.8, is still accepted, so old+          URLs keep working;+        - `compat`: SegWit nested in pay-to-script-hash (P2SH-P2WPKH), addresses+          starting with `3`, as BIP 49 wallets use;+        - `segwit`: native SegWit (P2WPKH), addresses starting with `bc1q`, as BIP+          84 wallets use.++        An xpub does not record which kind its wallet uses, and each kind gives+        different addresses from the same key. When the wallet has no+        transactions with this kind but has some with another, the request+        fails with 400 and a message that names the right kind, rather than+        answering with an empty wallet.+      required: false+      schema:+        type: string+        enum: [legacy, compat, segwit, normal]+        default: legacy++    prettyParam:+      name: pretty+      in: query+      description: |+        Indent the JSON, for people to read. Responses are compact by default to+        save bandwidth.+      required: false+      schema:+        type: boolean++    binfoAddr:+      name: address+      in: path+      description: |+        An address or an extended public key.+      required: true+      schema:+        type: string+      example: qqmaqu9nceck5ct5pqrjy4ulhhw66wdlkv2s7f2l9u++    binfoActive:+      name: active+      in: query+      description: |+        Addresses and extended public keys of the wallet, separated by `|` or `,`.+        Extended public keys here derive legacy (P2PKH) addresses.+      required: false+      schema:+        type: string+        example: "xpub6By8JDaPr5L6oHfgQDc47quD69qH1hTwnFYbuia8paiYxSE9u84K\+                 ZfYqn6xLMUqxKK3wNpsgP4Kwu1gzXHD5xBxj5HrLposEYL6PwZzpAMZ"++    binfoActiveP2SH:+      name: activeP2SH+      in: query+      description: |+        Extended public keys that derive SegWit addresses nested in+        pay-to-script-hash (P2SH-P2WPKH), separated by `|` or `,`.+      required: false+      schema:+        type: string++    binfoActiveBech32:+      name: activeBech32+      in: query+      description: |+        Extended public keys that derive native SegWit (P2WPKH) addresses, separated+        by `|` or `,`.+      required: false+      schema:+        type: string++    binfoOnlyShow:+      name: onlyShow+      in: query+      description: |+        The addresses and extended public keys, among the active ones, whose+        balances to list and from whose point of view to compute the result of each+        transaction, separated by `|` or `,`. All the active ones if left out.+      required: false+      schema:+        type: string++    binfoFilter:+      name: filter+      in: query+      description: |+        Which transactions to list (`0` or `6` for all of them):++        - `1`: those that send coins out of the wallet;+        - `2`: those that bring coins into the wallet;+        - `3`: those that move coins within the wallet, changing its balance by+          the fee only;+        - `5`: confirmed ones;+        - `7`: unconfirmed ones.+      required: false+      schema:+        type: integer+        enum: [0,1,2,3,5,7]+      example: 5++    binfoLimit:+      name: limit+      in: query+      description: |+        How many items to return at most. The default and maximum are those of the+        server.+      required: false+      schema:+        type: integer+        example: 2++    binfoCount:+      name: n+      in: query+      description: |+        How many transactions to return at most. The default and maximum are those+        of the server for full transactions.+      required: false+      schema:+        type: integer+        example: 2++    binfoOffset:+      name: offset+      in: query+      description: |+        How many items to skip at the start. Above the server's maximum, the request+        fails with 400.+      required: false+      schema:+        type: integer++    binfoConfirmations:+      name: confirmations+      in: query+      description: |+        Only return outputs with at least this many confirmations. `0`, the default,+        includes unconfirmed ones.+      required: false+      schema:+        type: integer++    binfoNoCompact:+      name: noCompact+      in: query+      description: |+        Keep the outputs of incoming transactions that pay to other addresses. They+        are left out by default, as they belong to other people and can be many.+        `no_compact` is accepted as another name for it; `noCompact` wins if both+        are given.+      required: false+      schema:+        type: boolean+        example: false++    binfoCurrency:+      name: currency+      in: query+      description: |+        The currency for the `info` section, such as `USD`, `EUR` or `GBP`, in any+        case. `USD` by default. A currency that the price service does not list+        is refused. Until the server has prices, which it does not if the price+        service cannot be reached, every currency is accepted with a price of+        zero.+      required: false+      schema:+        type: string+        example: eur++    binfoCashAddr:+      name: cashaddr+      in: query+      description: |+        Write Bitcoin Cash addresses in CashAddr format. They are in the legacy+        format by default, as Blockchain.info writes them, so that software written+        for it reads them.+      required: false+      schema:+        type: boolean+        example: true++    binfoTxIdIndex:+      name: txidindex+      in: query+      description: |+        Identify transactions by their id (`true`) rather than by a short number+        (the default).++        Blockchain.info identifies transactions by a number, and software written+        for it expects one. The server derives it from the transaction id: the first+        64 bits, shifted right by 11, so that it fits in the 53 bits that numbers in+        JavaScript hold exactly. Two transactions can share a number, rarely; the+        ids never collide, so use them when you can.+      required: false+      schema:+        type: boolean+        example: true++    binfoBlockId:+      name: block+      in: path+      description: |+        A block hash in hexadecimal, or a block height.+      required: true+      schema:+        oneOf:+          - type: string+          - type: integer+      example: 50000++    binfoTxId:+      name: txid+      in: path+      description: |+        A transaction id in hexadecimal, or the short number of a transaction (see+        `txidindex`).+      required: true+      schema:+        oneOf:+          - type: string+          - type: integer+      example: 369c2a28fc84b060e4423e26ac898fa4ce9eda8943a36d0a48df66e6a6275aa6++    binfoJsonFormat:+      name: format+      in: query+      description: |+        Only `json` is accepted. Software written for Blockchain.info often sends+        `format=json`, so it is accepted; any other value is refused rather than+        answered in JSON anyway.+      required: false+      schema:+        type: string+        enum: [json]++    binfoFormat:+      name: format+      in: query+      description: |+        `json`, the default, or `hex` for the data serialized as in the peer-to-peer+        protocol.+      required: false+      schema:+        type: string+        enum:+          - json+          - hex++    binfoStart:+      name: start+      in: query+      description: |+        The first day of the history, as `dd-mm-yyyy` or `dd/mm/yyyy`.+      required: true+      schema:+        type: string+        example: 20-10-2020++    binfoEnd:+      name: end+      in: query+      description: |+        The last day of the history, included, as `dd-mm-yyyy` or `dd/mm/yyyy`.+      required: true+      schema:+        type: string+        example: 09-04-2021
+ test/Haskoin/Store/BlockStore/PendingSpec.hs view
@@ -0,0 +1,94 @@+-- | 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)
+ test/Haskoin/Store/BlockStore/SyncSpec.hs view
@@ -0,0 +1,129 @@+-- | The state machine of the block download, driven by random sequences of+-- events, with its rules checked after every step.+module Haskoin.Store.BlockStore.SyncSpec (spec) where++import Data.ByteString.Char8 qualified as C8+import Data.List (nub)+import Data.Maybe (fromMaybe, isNothing)+import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, diffUTCTime)+import Haskoin (BlockHash (..), sha256)+import Haskoin.Store.BlockStore.Sync+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++-- | Something that happens to the block store, from one of three peers.+data Event+  = Start Int+  | Release Int+  | Request [Int]+  | Import Int+  | Forget+  | Wait Int+  deriving stock (Show)++instance Arbitrary Event where+  arbitrary =+    frequency+      [ (2, Start <$> peer),+        (1, Release <$> peer),+        (3, Request <$> listOf block),+        (3, Import <$> block),+        (1, pure Forget),+        (2, Wait <$> choose (0, 30))+      ]+    where+      peer = choose (0, 2)+      block = choose (0, 9)++-- | A state and the time it is at.+type World = (UTCTime, Sync Int)++hash :: Int -> BlockHash+hash = BlockHash . sha256 . C8.pack . show++t0 :: UTCTime+t0 = UTCTime (fromGregorian 2026 11 9) 0++-- | Apply an event as the block store does: imports only what 'accept'+-- takes.+step :: World -> Event -> World+step (now, s) = \case+  Start p -> (now, fromMaybe s (start now p s))+  Release p -> (now, snd (release p s))+  Request bs -> (now, request (map hash bs) s)+  Import b -> case syncingPeer s of+    Just p | accept p (hash b) s == Requested -> (now, imported now (hash b) s)+    _ -> (now, s)+  Forget -> (now, forgetRequests s)+  Wait secs -> (addUTCTime (fromIntegral secs) now, s)++-- | Every state that a sequence of events goes through.+run :: [Event] -> [World]+run = scanl step (t0, Idle)++timeout :: NominalDiffTime+timeout = 60++spec :: Spec+spec = describe "Block download" $ do+  prop "never has a block outstanding twice" $ \es ->+    all (\(_, s) -> nub (outstanding s) == outstanding s) (run es)++  prop "has a syncing peer only after starting, and keeps it until released" $ \es ->+    and+      [ case e of+          Start _ -> syncingPeer s' == syncingPeer s || isNothing (syncingPeer s)+          Release p+            | syncingPeer s == Just p -> syncingPeer s' == Nothing+          _ -> syncingPeer s' == syncingPeer s+      | ((_, s), e, (_, s')) <- zip3 (run es) es (drop 1 (run es))+      ]++  prop "accepts a block only from the syncing peer, and only if outstanding" $ \es ->+    forAll (choose (0, 2)) $ \peer -> forAll (hash <$> choose (0, 9)) $ \h ->+      let (_, s) = last (run es)+       in accept peer h s === case syncingPeer s of+            Just q+              | q /= peer -> NotSyncingPeer+              | h `elem` outstanding s -> Requested+              | otherwise -> NotRequested+            Nothing -> NotSyncingPeer++  prop "keeps the requested blocks in the order they were asked for" $ \es bs ->+    let (_, s) = last (run es)+        s' = request (map hash bs) s+     in case syncingPeer s of+          Nothing -> outstanding s' === []+          Just _ -> take (length (outstanding s)) (outstanding s') === outstanding s++  prop "times out only a peer that delivered nothing for longer than the timeout" $ \es ->+    and+      [ case s of+          Idle -> isNothing (timedOut timeout now s)+          Syncing d -> (timedOut timeout now s == Just d.peer) == (now `diffUTCTime` d.time > timeout)+      | (now, s) <- run es+      ]++  prop "asks for more blocks exactly when the window has room" $ \es (Positive window) ->+    let (_, s) = last (run es)+     in wantsMore window s === case syncingPeer s of+          Just p | needsMoreBlocks window (length (outstanding s)) -> Just p+          _ -> Nothing++  describe "needsMoreBlocks" $ do+    it "asks once fewer than a quarter of the window remain" $ do+      needsMoreBlocks 64 16 `shouldBe` False+      needsMoreBlocks 64 15 `shouldBe` True+    it "asks once none remain, even with a window below four" $ do+      needsMoreBlocks 1 1 `shouldBe` False+      needsMoreBlocks 1 0 `shouldBe` True+      needsMoreBlocks 3 0 `shouldBe` True++  it "resets the clock of the peer when it delivers a block" $ do+    let s = Syncing (Download (0 :: Int) t0 [])+        later = addUTCTime 50 t0+        s' = imported later (hash 1) (request [hash 1] s)+    timedOut timeout (addUTCTime 100 t0) s `shouldBe` Just 0+    timedOut timeout (addUTCTime 100 t0) s' `shouldBe` Nothing
− test/Haskoin/Store/BlockStoreSpec.hs
@@ -1,16 +0,0 @@-module Haskoin.Store.BlockStoreSpec (spec) where--import Haskoin.Store.BlockStore (needsMoreBlocks)-import Test.Hspec--spec :: Spec-spec =-  describe "needsMoreBlocks" $ do-    it "asks for more blocks once fewer than a quarter of the window remain" $ do-      -- 64 / 4 = 16-      needsMoreBlocks 64 16 `shouldBe` False-      needsMoreBlocks 64 15 `shouldBe` True-    it "asks for more blocks once none remain, even with a window below four" $ do-      needsMoreBlocks 1 1 `shouldBe` False-      needsMoreBlocks 1 0 `shouldBe` True-      needsMoreBlocks 3 0 `shouldBe` True
+ test/Haskoin/Store/Cache/ProtocolSpec.hs view
@@ -0,0 +1,158 @@+-- | The protocol of stores sharing one Redis, against a model of Redis:+-- several stores catch up with a chain in which every block changes one+-- cached xpub, in a random interleaving, and decide with the functions of+-- "Haskoin.Store.Cache.Protocol" what to do at each step.+module Haskoin.Store.Cache.ProtocolSpec (spec) where++import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IntMap+import Haskoin.Store.Cache.Protocol+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++-- | The claim of the change of a block.+data Key = Absent | BusyBy Int | DoneKey+  deriving stock (Eq, Show)++-- | Redis: the height the entry is stamped with, and the claims.+data Redis = Redis+  { stamp :: !(Maybe Integer),+    claims :: !(IntMap Key)+  }+  deriving stock (Show)++-- | A store: its height, the change it holds, and changes to claim again.+data Store = Store+  { height :: !Int,+    holding :: !(Maybe Int),+    retry :: ![Int]+  }++-- | What happened.+data Outcome = Outcome+  { -- | the stamp written by each write, by the store and change it was for+    writes :: ![(Int, Int, Integer)],+    -- | claims given up+    releases :: !Int,+    redis :: !Redis,+    -- | whether every store reached the tip with nothing left to do+    finished :: !Bool+  }+  deriving stock (Show)++-- | Try to claim the change of a block, and do what 'afterClaim' says.+tryClaim :: Int -> Int -> Redis -> Store -> (Redis, Store)+tryClaim s i r st =+  let found = case IntMap.findWithDefault Absent i r.claims of+        Absent -> Claimed+        DoneKey -> Done+        BusyBy _ -> Busy+   in case afterClaim found of+        ReloadNow -> (r {claims = IntMap.insert i (BusyBy s) r.claims}, st {holding = Just i})+        Skip -> (r, st)+        ClaimLater -> (r, st {retry = st.retry <> [i]})++-- | A step of a store, if it can take one: reload for the change it holds,+-- claim a change again, or import the next block and claim its change.+steps :: Int -> Int -> Outcome -> Store -> [(Outcome, Store)]+steps tip s o st = case st.holding of+  Just i ->+    let h = toInteger st.height+        -- the entry has a transaction the store has not seen if it is+        -- stamped with a later block, as each block adds one+        unknown = maybe False (> h) o.redis.stamp+        reload = if mayReplace o.redis.stamp (fromIntegral st.height) unknown then Reloaded else Behind+        r = o.redis+     in case afterReload reload of+          Finish ->+            [ ( o+                  { writes = o.writes <> [(s, i, h)],+                    redis = r {stamp = Just h, claims = IntMap.insert i DoneKey r.claims}+                  },+                st {holding = Nothing}+              )+            ]+          Release ->+            [ ( o {releases = o.releases + 1, redis = r {claims = IntMap.insert i Absent r.claims}},+                st {holding = Nothing}+              )+            ]+          Hold -> []+  Nothing ->+    [ let (r, st') = tryClaim s i o.redis st {retry = rest} in (o {redis = r}, st')+    | i : rest <- [st.retry]+    ]+      <> [ let (r, st') = tryClaim s (st.height + 1) o.redis st {height = st.height + 1}+            in (o {redis = r}, st')+         | st.height < tip+         ]++-- | Run stores that start at the given heights until none can take a step,+-- picking the next step at random. The changes of the blocks up to the+-- lowest store were applied before.+simulate :: Int -> [Int] -> Gen Outcome+simulate tip starts =+  simulateFrom+    (Redis (Just (toInteger (minimum starts))) (IntMap.fromList [(i, DoneKey) | i <- [1 .. minimum starts]]))+    tip+    starts++-- | Like 'simulate', from a state of Redis.+simulateFrom :: Redis -> Int -> [Int] -> Gen Outcome+simulateFrom r0 tip starts = go (10000 :: Int) start0 (IntMap.fromList (zip [0 ..] stores0))+  where+    stores0 = [Store h Nothing [] | h <- starts]+    start0 = Outcome {writes = [], releases = 0, redis = r0, finished = False}+    go 0 o _ = return o+    go n o stores =+      case [(s, x) | (s, st) <- IntMap.toList stores, x <- steps tip s o st] of+        [] -> return o {finished = True}+        xs -> do+          (s, (o', st')) <- elements xs+          go (n - 1) o' (IntMap.insert s st' stores)++-- | A small chain and up to four stores at random heights on it.+chain :: Gen (Int, [Int])+chain = do+  tip <- choose (1, 12)+  stores <- choose (1, 4)+  starts <- vectorOf stores (choose (0, tip))+  return (tip, starts)++spec :: Spec+spec = describe "Stores sharing Redis" $ do+  prop "all reach the tip with nothing left to do" $+    forAll chain $+      \(tip, starts) -> forAll (simulate tip starts) $ \o -> o.finished++  prop "end with the entry of the tip" $+    forAll chain $ \(tip, starts) -> forAll (simulate tip starts) $ \o ->+      o.redis.stamp === Just (toInteger tip)++  prop "never replace an entry with one that knows less" $+    forAll chain $ \(tip, starts) -> forAll (simulate tip starts) $ \o ->+      let stamps = [h | (_, _, h) <- o.writes]+       in and (zipWith (<=) stamps (drop 1 stamps))++  prop "write once per change, and again only for a change given up" $+    forAll chain $ \(tip, starts) -> forAll (simulate tip starts) $ \o ->+      length o.writes <= tip - minimum starts + o.releases++  prop "leave nothing to do for stores that catch up after the first" $+    forAll chain $ \(tip, starts) ->+      forAll (simulate tip [minimum starts]) $ \first ->+        forAll (simulateFrom first.redis tip starts) $ \rest ->+          rest.writes === []++  it "claims, skips or tries again as the claim says" $+    map afterClaim [minBound .. maxBound] `shouldBe` [ReloadNow, Skip, ClaimLater]++  it "finishes, releases or holds as the reload says" $+    map afterReload [minBound .. maxBound] `shouldBe` [Finish, Release, Hold]++  it "replaces an entry of the same height or lower, never a higher one" $ do+    mayReplace Nothing 5 False `shouldBe` True+    mayReplace (Just 5) 5 False `shouldBe` True+    mayReplace (Just 6) 5 False `shouldBe` False+    mayReplace (Just 4) 5 True `shouldBe` False
test/Haskoin/Store/CacheSpec.hs view
@@ -1,24 +1,46 @@ module Haskoin.Store.CacheSpec (spec) where -import Data.List (sort)-import Haskoin.Store.Cache (blockRefScore, scoreBlockRef)-import Haskoin.Store.Data (BlockRef (..))-import Test.Hspec (Spec, describe)+import Data.ByteString qualified as BS+import Data.ByteString.Char8 qualified as C8+import Data.List (nub, sort)+import Haskoin (bch, btc, deriveXPubKey, makeXPrvKey, prepareContext)+import Haskoin.Store.Cache (blockRefScore, cachePrefix, fullReloadAfter, fullReloadEvery, scoreBlockRef)+import Haskoin.Store.Data (BlockRef (..), DeriveType (..), XPubSpec (..))+import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy) import Test.Hspec.QuickCheck (prop) import Test.QuickCheck (Gen, choose, forAll, listOf, oneof)  spec :: Spec spec = do   describe "Score for block reference" $ do-    prop "sorts correctly" $+    prop "sorts newest first" $       forAll arbitraryBlockRefs $ \ts ->         let scores = map blockRefScore (sort ts)          in sort scores == reverse scores     prop "respects identity" $       forAll arbitraryBlockRef $ \b ->-        let score = blockRefScore b-            ref = scoreBlockRef score-         in ref == b+        scoreBlockRef (blockRefScore b) == b+  describe "Keys" $+    it "start with a prefix of their own for each network" $ do+      cachePrefix btc `shouldBe` "hs2:btc:"+      cachePrefix bch `shouldBe` "hs2:bch:"+      -- no key of the old cache can start like a key of this one, nor the+      -- other way round: its keys are these names, or one of the one-letter+      -- prefixes followed by a serialized xpub or address+      let old = ["head", "max", "mempool", "lock", "xlock-", "b", "t", "u", "a", "i"]+          p = cachePrefix btc+      filter (\o -> o `BS.isPrefixOf` p || p `BS.isPrefixOf` o) old `shouldBe` []++  prepareContext $ \ctx ->+    describe "Scheduled full reloads" $+      it "come every five to ten minutes, at intervals spread over xpubs" $ do+        let seed i = C8.pack ("seed of the test xpub " <> show i)+            xpub i = XPubSpec (deriveXPubKey ctx (makeXPrvKey (seed i))) DeriveNormal+            after = map (fullReloadAfter . xpub) [1 .. 200 :: Int]+            half = fullReloadEvery `div` 2+        after `shouldSatisfy` all (\a -> a >= half && a < fullReloadEvery)+        -- each of the five minutes of the range has some of them+        sort (nub [(a - half) `div` 60000 | a <- after]) `shouldBe` [0 .. 4]  arbitraryBlockRefs :: Gen [BlockRef] arbitraryBlockRefs = listOf arbitraryBlockRef
test/Haskoin/Store/CommonSpec.hs view
@@ -1,21 +1,37 @@-{-# LANGUAGE OverloadedRecordDot #-}- module Haskoin.Store.CommonSpec (spec) where  import Conduit (runConduit, runConduitPure, sinkList, takeC, yieldMany, (.|))-import Control.Monad.State.Strict (State, modify', runState)-import Data.List (sortOn)+import Control.Monad.State.Strict (State, evalState, execState, modify', runState)+import Data.ByteString.Char8 qualified as C8+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.IntSet qualified as IntSet+import Data.List (nub, sortOn) import Data.Ord (Down (..))+import Data.Serialize (encode) import Data.Word (Word32)+import Haskoin (Address (..), OutPoint (..), TxHash (..), ripemd160, sha256) import Haskoin.Store.Common   ( Limits (..),     applyLimit,     applyLimits,     applyLimitsC,     deOffset,+    joinDescStreams,     mergeDescGroups,+    scanXPubBals,     streamThings,+    txRefKey,+    unspentKey,   )+import Haskoin.Store.Data+  ( Balance (..),+    BlockRef (..),+    TxRef (..),+    Unspent (..),+    XPubBal (..),+    zeroBalance,+  ) import Test.Hspec import Test.QuickCheck @@ -30,8 +46,8 @@     it "reads a number of rows in proportion to the rows taken" $       property $ \(NonNegative n) ->         let xs = [1 .. 100000 :: Int]-            (ys, reads) = paged 16 xs (Just n)-         in ys === take n xs .&&. counterexample (show reads) (reads <= 4 * n + 2048)+            (ys, rowsRead) = paged 16 xs (Just n)+         in ys === take n xs .&&. counterexample (show rowsRead) (rowsRead <= 4 * n + 2048)   describe "mergeDescGroups" $ do     it "pages like sorting the first offset plus limit items of every stream" $       property $ \(Streams ss) ->@@ -45,6 +61,137 @@           limits = Limits 2 0 Nothing        in merged limits ss `shouldBe` [(9, 3), (9, 1)] +  describe "scanXPubBals" $ do+    it "keeps exactly the gap after the last used address of each chain, and the initial block" $+      property $ \(Wallet igap gap ext chg) ->+        let paths = map (.path) (scan igap gap ext chg)+         in paths === modelPaths igap gap ext chg+    it "reads no address past the gap, beyond the initial block" $+      property $ \(Wallet igap gap ext chg) ->+        let seen = execState (scanXPubBals (countReads ext chg) igap gap (chain 0) (chain 1)) []+            limit m used+              | m == 0 = max (fromIntegral igap - 1) (lastReached igap gap used + fromIntegral gap)+              | otherwise = lastReached 0 gap used + fromIntegral gap+            ok (m, n) = n <= limit m (if m == 0 then ext else chg)+         in counterexample (show seen) (all ok seen)+    it "reads only the initial block of an unused xpub" $+      property $ \(NonNegative igap) (Positive gap) ->+        let seen = execState (scanXPubBals (countReads [] []) (igap `mod` 40) (gap `mod` 40) (chain 0) (chain 1)) []+         in sortOn id seen === [(0, n) | n <- [0 .. fromIntegral (igap `mod` 40) - 1]]+    it "counts the unused addresses at the end of the initial block" $+      -- index 0 used, initial gap 20, gap 32: the store looks at indices 1 to+      -- 32 and no further, while it used to read up to index 83+      let paths = map (.path) (scan 20 32 [0] [])+       in maximum [n | [0, n] <- paths] `shouldBe` 32+    it "does not see a payment 34 addresses past the last used one" $ do+      -- the wallet of the Blockchain.com comparison: receive indices 0 to 35+      -- used, then a payment to index 69+      let ext = [0 .. 35] <> [69]+          seen g = [n | XPubBal {path = [0, n], balance = b} <- scan 20 g ext [], b.txs > 0]+      maximum (seen 32) `shouldBe` 35+      maximum (seen 34) `shouldBe` 69+  describe "order of ties" $ do+    -- RocksDB and Redis both order entries of the same block position by+    -- their serialized bytes; the merges must agree with them+    it "puts transactions of one position in the order of their bytes" $+      property $ \(NonNegative t) (Positive n) ->+        let refs = [TxRef {block = MemRef t, txid = TxHash (sha256 (encode i))} | i <- [1 .. n `mod` 20 :: Int]]+         in map (.txid) (sortOn (Down . txRefKey) refs) === map (.txid) (sortOn (encode . (.txid)) refs)+    it "puts outputs of one position in the order of their bytes" $+      property $ \(Positive n) ->+        forAll (vectorOf (n `mod` 20) (choose (0, 70000))) $ \is ->+          let h = TxHash (sha256 ("tx" :: C8.ByteString))+              unspent i =+                Unspent {block = BlockRef 7 1, outpoint = OutPoint h i, value = 1, script = "", address = Nothing}+              us = map unspent (nub is)+           in map (.outpoint) (sortOn (Down . unspentKey) us) === map (.outpoint) (sortOn (encode . (.outpoint)) us)+  describe "joinDescStreams with txRefKey" $ do+    it "merges without duplicates mempool transactions of the same second" $+      property $ \(TxStreams all' ss) ->+        let merged' = runConduitPure $ joinDescStreams txRefKey (map yieldMany ss) .| sinkList+            want = sortOn (Down . txRefKey) (nub (concat ss))+         in counterexample (show (length all')) (merged' === want)++-- | Addresses used on each chain, with the initial gap and the gap.+data Wallet = Wallet Word32 Word32 [Int] [Int]+  deriving (Show)++instance Arbitrary Wallet where+  arbitrary = do+    igap <- choose (0, 30)+    gap <- choose (1, 40)+    let used = fmap (IntSet.toList . IntSet.fromList) . listOf $ frequency [(4, choose (0, 40)), (1, choose (0, 150))]+    Wallet igap gap <$> used <*> used++-- | A fake address for a path.+fakeAddr :: Word32 -> Word32 -> Address+fakeAddr m n = PubKeyAddress (ripemd160 (C8.pack (show (m, n))))++chain :: Word32 -> [(Word32, Address)]+chain m = [(n, fakeAddr m n) | n <- [0 ..]]++-- | Balances of fake addresses: the used ones have one transaction.+balancesOf :: [Int] -> [Int] -> [Address] -> [Balance]+balancesOf ext chg = map f+  where+    used = HashSet.fromList ([fakeAddr 0 (fromIntegral n) | n <- ext] <> [fakeAddr 1 (fromIntegral n) | n <- chg])+    f a+      | a `HashSet.member` used = (zeroBalance a) {txs = 1, received = 1}+      | otherwise = zeroBalance a++scan :: Word32 -> Word32 -> [Int] -> [Int] -> [XPubBal]+scan igap gap ext chg =+  evalState (scanXPubBals (return . balancesOf ext chg) igap gap (chain 0) (chain 1)) ()++-- | Read balances, recording the paths read.+countReads :: [Int] -> [Int] -> [Address] -> State [(Word32, Int)] [Balance]+countReads ext chg as = do+  modify' ([p | a <- as, Just p <- [HashMap.lookup a pathOf]] <>)+  return (balancesOf ext chg as)++-- | Path of each fake address, derived once.+pathOf :: HashMap.HashMap Address (Word32, Int)+pathOf = HashMap.fromList [(fakeAddr m n, (m, fromIntegral n)) | m <- [0, 1], n <- [0 .. 400]]++-- | Last used index that a wallet following the gap reaches, or -1.+lastReached :: Word32 -> Word32 -> [Int] -> Int+lastReached igap gap used = go start+  where+    start = maximum ((-1) : filter (< fromIntegral igap) used)+    go l = case filter (\u -> u > l && u <= l + fromIntegral gap) used of+      [] -> l+      us -> go (maximum us)++-- | Paths that an exact gap gives: up to the gap after the last used address+-- of each chain, and the whole initial block of the external chain.+modelPaths :: Word32 -> Word32 -> [Int] -> [Int] -> [[Word32]]+modelPaths igap gap ext chg+  | not (any (< fromIntegral igap) ext) = [[0, fromIntegral n] | n <- [0 .. fromIntegral igap - 1 :: Int]]+  | otherwise =+      upto 0 (max (fromIntegral igap - 1) (lastReached igap gap ext + fromIntegral gap))+        <> upto 1 (lastReached 0 gap chg + fromIntegral gap)+  where+    upto m l = [[m, fromIntegral n] | n <- [0 .. l]]++-- | Transaction references and streams of subsets of them, each sorted as the+-- database sorts the transactions of an address, with many mempool+-- transactions that arrived in the same second.+data TxStreams = TxStreams [TxRef] [[TxRef]]+  deriving (Show)++instance Arbitrary TxStreams where+  arbitrary = do+    n <- choose (0, 40)+    refs <- mapM ref [1 .. n]+    k <- choose (1, 5)+    ss <- vectorOf k (sublistOf refs)+    return $ TxStreams refs (map (sortOn (Down . txRefKey)) ss)+    where+      ref :: Int -> Gen TxRef+      ref i = do+        b <- oneof [MemRef <$> choose (1, 3), BlockRef <$> choose (1, 3) <*> pure (fromIntegral i)]+        return TxRef {block = b, txid = TxHash (sha256 (encode i))}+ -- | The paging of xpub unspent outputs before 'mergeDescGroups': the first -- offset plus limit items of each stream, all sorted, then the page. reference :: Limits -> [[(Int, Int)]] -> [(Int, Int)]@@ -54,7 +201,7 @@ merged :: Limits -> [[(Int, Int)]] -> [(Int, Int)] merged limits ss =   runConduitPure $-    mergeDescGroups fst quota (map yieldMany ss)+    mergeDescGroups fst id quota (map yieldMany ss)       .| applyLimitsC limits       .| sinkList   where
+ test/Haskoin/Store/Database/MigrateSpec.hs view
@@ -0,0 +1,490 @@+{-# LANGUAGE DeriveAnyClass #-}++module Haskoin.Store.Database.MigrateSpec (spec) where++import Control.Monad (foldM, forM_, void, when)+import Data.ByteString qualified as B+import Data.Either (isRight, rights)+import Data.IORef (modifyIORef', newIORef, readIORef)+import Data.IntMap.Strict qualified as IntMap+import Data.List (sort)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, listToMaybe)+import Data.Serialize (decode, encode)+import Data.Text qualified as T+import Data.Word (Word16, Word32, Word64)+import Database.RocksDB (BatchOp (..))+import Database.RocksDB.Query (insert, insertOp, retrieve, writeBatch)+import Haskoin+  ( BlockHeader (..),+    Ctx,+    Network,+    OutPoint (..),+    Tx (..),+    TxHash (..),+    TxIn (..),+    TxOut (..),+    btcRegTest,+    headerHash,+    prepareContext,+    scriptToAddressBS,+    sha256,+    txHash,+  )+import Haskoin.Store.Data (Balance (..), BlockData (..), BlockRef (..), Spender (..), TxData (..))+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Migrate+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    mempoolAddrCF,+    mempoolCF,+    mempoolInsertOps,+    spenderCF,+    txCF,+  )+import Haskoin.Store.Database.Types (MigrationKey (..), SpenderKey (..), SpenderVal (..), VersionKey (..))+import Haskoin.Store.Histories+import Haskoin.Store.TestBlocks (script)+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck (forAll, ioProperty, property, tabulate, vectorOf)+import UnliftIO (Exception, throwIO, try, withSystemTempDirectory)++spec :: Spec+spec = prepareContext $ \ctx -> describe "migration to the layered layout" $ do+  it "takes the mempool out of the chain's indices, keeping every answer" $+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+      -- A coin of a block; a payment from it in the mempool, with change;+      -- and a transaction that spends that change, also in the mempool.+      (hist, old) <- mixedDatabase ctx dir $ \db hist0 -> do+        let payment = Tx 2 [TxIn (coinOf hist0) "" maxBound] [TxOut 3000 (p2pkh 1), TxOut 2000 script] [] 0+            child = Tx 2 [TxIn (OutPoint (txHash payment) 1) "" maxBound] [TxOut 1500 (p2pkh 2)] [] 0+        foldM (addTx db) hist0 [payment, child]+      (miner, one, two) <- case rights (map (scriptToAddressBS net ctx) [script, p2pkh 1, p2pkh 2]) of+        [a, b, c] -> return (a, b, c)+        _ -> fail "a script has no address"+      let numbers bs a = fmap (\b -> (b.confirmed, b.unconfirmed, b.utxo, b.txs, b.received)) (lookup a bs)+      -- the coin is spent in the mempool: it no longer counts as confirmed+      numbers old.balances miner `shouldBe` Just (0, 0, 0, 3, 5000002000)+      numbers old.balances one `shouldBe` Just (0, 3000, 1, 1, 3000)+      numbers old.balances two `shouldBe` Just (0, 1500, 1, 1, 1500)+      openLayered net ctx dir $ \db -> do+        migrateLayered db `shouldReturn` Migration {moved = 2, batches = 1, checked = 2, rewritten = 0}+        retrieve db.db VersionKey `shouldReturn` Just layeredVersion+        (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Nothing+        -- the chain has the coin again, whole and unspent+        chainBalance <- fmap (\b -> (b.confirmed, b.unconfirmed, b.utxo, b.txs, b.received)) <$> Q.getBalance db miner+        chainBalance `shouldBe` Just (5000000000, 0, 1, 1, 5000000000)+        Q.getBalance db one `shouldReturn` Nothing+        Q.getBalance db two `shouldReturn` Nothing+        -- and with the mempool on top, every answer is what it was+        -- the queries read a database of the version that the store runs on+        settle db+        layered db (ask hist) `shouldReturn` old+        -- a second run finds nothing to do+        migrateLayered db `shouldReturn` Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}++  modifyMaxSuccess (const 50) $+    it "keeps every answer and leaves the chain as if the mempool had been wiped, for random histories" $+      property . forAll ((,) <$> vectorOf 45 anyNumber <*> vectorOf 12 anyNumber) $ \(seeds, last') -> ioProperty $ do+        withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+          -- anything at first, then payments, so that the mempool has something+          (hist, old) <- mixedDatabase ctx dir $ \db hist0 -> foldM (step db) hist0 (seeds <> onlyPayments last')+          withSystemTempDirectory "haskoin-store-chain-" $ \dir' ->+            open20 net ctx dir' $ \fresh -> do+              -- the same blocks, in a database that never saw the mempool+              void (run fresh mixedRules.initBest) :: IO ()+              forM_ (reverse hist.chain) $ \(block, node) ->+                run fresh (\w -> mixedRules.importBlock w block node) >>= either (fail . show) (\_ -> return ())+              openLayered net ctx dir $ \db -> do+                done <- migrateLayeredWith defaultMigrationOptions {chunk = 3} db+                done.checked `shouldBe` length old.mempool+                done.moved `shouldBe` length old.mempool+                -- every answer is what it was+                -- the queries read a database of the version that the store runs on+                settle db+                layered db (ask hist) `shouldReturn` old+                -- the chain's column families are those of the fresh database,+                -- but for the times at which transactions were first seen+                migrated <- chainOf db+                expected <- chainOf fresh+                length expected.addressTxs `shouldSatisfy` (>= length hist.chain)+                migrated {firstSeen = []} `shouldBe` expected {firstSeen = []}+                -- the mempool area is the keys of its transactions, each worked+                -- out from that transaction alone: no more and no less+                moved <- catMaybes <$> mapM (M.getMempoolTx db . snd) old.mempool+                let records = [M.mempoolRecords net ctx time td | td <- moved, MemRef time <- [td.block]]+                length records `shouldBe` length old.mempool+                dump db (mempoolCF db.db) `shouldReturn` sort (concatMap (fst . M.recordEntries) records)+                dump db (mempoolAddrCF db.db) `shouldReturn` sort (M.addressArea records)+                -- without the mempool area, the answers are those of the chain+                void (M.wipeMempoolArea db)+                wiped <- layered db (ask hist)+                alone <- mixed fresh (ask hist)+                (wiped.balances, wiped.addressTxs, wiped.addressUnspents, wiped.unspents, wiped.mempool)+                  `shouldBe` (alone.balances, alone.addressTxs, alone.addressUnspents, alone.unspents, [])+          let inMempool = [td | (_, Just td) <- old.txs, not td.deleted, MemRef {} <- [td.block]]+              unconfirmed = map (txHash . (.tx)) inMempool+              chained = [() | td <- inMempool, i <- td.tx.inputs, i.outpoint.hash `elem` unconfirmed]+              bucket n+                | n == 0 = "none"+                | n <= 3 = "1 to 3"+                | n <= 9 = "4 to 9"+                | otherwise = "10 or more" :: String+          return+            . tabulate "mempool transactions" [bucket (length inMempool)]+            . tabulate "of them spending another" [bucket (length chained)]+            . tabulate "deleted transactions" [bucket (length [() | (_, Just td) <- old.txs, td.deleted])]+            $ tabulate "blocks" [bucket (length hist.chain)] True++  it "carries on after being interrupted, between any two writes" $+    forM_ [1 .. 6] $ \stopAt ->+      withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+        (hist, old) <- mixedDatabase ctx dir $ \db hist0 -> foldM (addTx db) hist0 (payments (coinOf hist0))+        length old.mempool `shouldBe` 8+        -- one transaction in each write, and the plug pulled after a few+        let pull n = when (n == stopAt) (throwIO Interrupted)+            interrupted = defaultMigrationOptions {chunk = 1, afterBatch = pull}+        openLayered net ctx dir $ \db -> do+          try (migrateLayeredWith interrupted db) `shouldReturn` Left Interrupted+          -- a store of version 20 would refuse the database from here on+          retrieve db.db VersionKey `shouldReturn` Just layeredVersion+          (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Just 20+        openLayered net ctx dir $ \db -> do+          done <- migrateLayered db+          done.moved `shouldBe` length old.mempool - stopAt+          done.checked `shouldBe` length old.mempool+          (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Nothing+          -- the queries read a database of the version that the store runs on+          settle db+          layered db (ask hist) `shouldReturn` old++  it "refuses a database that it cannot handle, and leaves it untouched" $ do+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir ->+      openLayered net ctx dir $ \db -> do+        migrateLayered db `shouldThrow` (== UnknownVersion Nothing)+        insert db.db VersionKey (19 :: Word32)+        migrateLayered db `shouldThrow` (== UnknownVersion (Just 19))+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+      -- the mempool lists a transaction that has no body+      let lost = TxHash (sha256 ("lost" :: B.ByteString))+      _ <- mixedDatabase ctx dir $ \db hist0 -> do+        hist1 <- foldM (addTx db) hist0 (take 5 (payments (coinOf hist0)))+        writeBatch db.db (mempoolInsertOps db.db lost 7)+        return hist1+      openLayered net ctx dir $ \db -> do+        migrateLayered db `shouldThrow` \case+          Inconsistent _ -> True+          _ -> False+        retrieve db.db VersionKey `shouldReturn` Just (20 :: Word32)+        dump db (mempoolAddrCF db.db) `shouldReturn` []+        -- the mempool column family has its list and its index by time only+        written <- dump db (mempoolCF db.db)+        written `shouldSatisfy` all ((`elem` ["\x08", "\x0b"]) . B.take 1 . fst)++  it "brings a database of any version that the store knows to the layout it runs on" $ do+    -- a new database+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir ->+      openLayered net ctx dir $ \db -> do+        upgradeDatabase (\_ -> return ()) db+          `shouldReturn` Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}+        retrieve db.db VersionKey `shouldReturn` Just currentVersion+    -- one of version 20 with a mempool, and then the same one again+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+      (hist, old) <- mixedDatabase ctx dir $ \db hist0 ->+        foldM (addTx db) hist0 (take 5 (payments (coinOf hist0)))+      forM_ [5, 0] $ \moved ->+        openLayered net ctx dir $ \db -> do+          done <- upgradeDatabase (\_ -> return ()) db+          done.moved `shouldBe` moved+          retrieve db.db VersionKey `shouldReturn` Just currentVersion+          (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Nothing+          layered db (ask hist) `shouldReturn` old+          M.wrongSummaries db `shouldReturn` []+    -- one of version 19, which had no index of the mempool by time+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do+      (hist, old) <- mixedDatabase ctx dir $ \db hist0 ->+        foldM (addTx db) hist0 (take 3 (payments (coinOf hist0)))+      openLayered net ctx dir $ \db -> do+        index <- dump db (mempoolCF db.db)+        writeBatch db.db $+          insertOp VersionKey (19 :: Word32)+            : [DelCF (mempoolCF db.db) k | (k, _) <- index, B.take 1 k == "\x0b"]+        done <- upgradeDatabase (\_ -> return ()) db+        done.moved `shouldBe` 3+        layered db (ask hist) `shouldReturn` old+    -- and one of a version that it does not know+    withSystemTempDirectory "haskoin-store-migrate-" $ \dir ->+      openLayered net ctx dir $ \db -> do+        insert db.db VersionKey (17 :: Word32)+        upgradeDatabase (\_ -> return ()) db `shouldThrow` (== UnknownVersion (Just 17))++  describe "the time of a confirmed transaction" $ do+    it "is the time of its block, not of when the store first saw it" $+      withSystemTempDirectory "haskoin-store-times-" $ \dir ->+        openLayered net ctx dir $ \db -> do+          hist <- start layeredRules db+          let tx = Tx 2 [TxIn (coinOf hist) "" maxBound] [TxOut 4000 (p2pkh 1)] [] 0+              stored = do+                rows <- dump db (txCF db.db)+                return [td :: TxData | (_, v) <- rows, Right td <- [decode v], txHash td.tx == txHash tx]+          (.result) <$> apply layeredRules db (Pay tx 1000) `shouldReturn` Right "True"+          -- in the mempool it has the time at which it came+          fmap (.timestamp) <$> M.getMempoolTx db (txHash tx) `shouldReturn` Just 1000+          mined <- blockOn db [tx] >>= apply layeredRules db+          isRight mined.result `shouldBe` True+          best <- Q.getBestBlock db >>= maybe (fail "no best block") (Q.getBlock db)+          time <- maybe (fail "no block") (return . fromIntegral . (.header.timestamp)) best+          time `shouldNotBe` (1000 :: Word64)+          map (\td -> (td.block, td.timestamp)) <$> stored `shouldReturn` [(BlockRef 2 1, time)]+          wrongTimes db `shouldReturn` []+          -- and when the block is reverted it goes back to the mempool+          -- with that time, which every store has for it+          reverted <- apply layeredRules db (Revert (headerHash (maybe (error "no block") (.header) best)))+          isRight reverted.result `shouldBe` True+          fmap (.timestamp) <$> M.getMempoolTx db (txHash tx) `shouldReturn` Just time+    modifyMaxSuccess (const 20) $+      it "is the time of its block, after any history" $+        -- whatever a store saw in its mempool before, and through blocks+        -- that are reverted and replaced+        property . forAll (vectorOf 60 anyNumber) $ \seeds -> ioProperty $+          withSystemTempDirectory "haskoin-store-times-" $ \dir ->+            openLayered net ctx dir $ \db -> withShadow net ctx $ \shadow -> do+              hist0 <- start layeredRules db+              void (foldM (walk shadow db) hist0 seeds)+              late <- wrongTimes db+              late `shouldBe` []+    modifyMaxSuccess (const 20) $+      it "is set by the migration to version 22, and nothing else is changed" $+        -- A database as the layered rules write it has the time of its+        -- block for every confirmed transaction. Some of them are given+        -- another time, as a store of version 21 that saw them in its+        -- mempool had, and the database is marked as of that version. The+        -- migration must give back the database as it was, byte for byte.+        property . forAll (vectorOf 40 anyNumber) $ \seeds -> ioProperty $+          withSystemTempDirectory "haskoin-store-times-" $ \dir ->+            openLayered net ctx dir $ \db -> withShadow net ctx $ \shadow -> do+              hist0 <- start layeredRules db+              void (foldM (walk shadow db) hist0 seeds)+              original <- dump db (txCF db.db)+              let confirmed = [(k, td :: TxData) | (k, v) <- original, Right td <- [decode v], inChain td]+                  -- every other one, and all of them when there are few+                  picked = [x | (i, x) <- zip [0 :: Int ..] confirmed, even i || length confirmed < 4]+              writeBatch db.db $+                insertOp VersionKey layeredVersion+                  : [PutCF (txCF db.db) k (encode (later td)) | (k, td) <- picked]+              wrongTimes db >>= (`shouldBe` length picked) . length+              done <- upgradeDatabase (\_ -> return ()) db+              done.rewritten `shouldBe` length picked+              retrieve db.db VersionKey `shouldReturn` Just currentVersion+              (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Nothing+              dump db (txCF db.db) `shouldReturn` original+              -- and again, it finds nothing to do+              upgradeDatabase (\_ -> return ()) db+                `shouldReturn` Migration {moved = 0, batches = 0, checked = 0, rewritten = 0}+    modifyMaxSuccess (const 20) $+      it "is in a record that is written once, and not when its outputs are spent" $+        -- After every step of a history, the record of each transaction+        -- that was confirmed before the step and still is, is the same+        -- bytes as before: blocks that spend its outputs, and blocks that+        -- are reverted, write keys of their own and leave it alone. No+        -- record has spenders in it.+        property . forAll (vectorOf 60 anyNumber) $ \seeds -> ioProperty $+          withSystemTempDirectory "haskoin-store-times-" $ \dir ->+            openLayered net ctx dir $ \db -> withShadow net ctx $ \shadow -> do+              hist0 <- start layeredRules db+              let records = do+                    rows <- dump db (txCF db.db)+                    return (Map.fromList [(k, v) | (k, v) <- rows, Right td <- [decode v], inChain td])+                  turn (hist, was) seed = do+                    hist' <- walk shadow db hist seed+                    now <- records+                    Map.filter id (Map.intersectionWith (/=) was now) `shouldBe` Map.empty+                    return (hist', now)+              first <- records+              (_, final) <- foldM turn (hist0, first) seeds+              [td.spenders | v <- Map.elems final, Right (td :: TxData) <- [decode v], not (IntMap.null td.spenders)]+                `shouldBe` []+    it "has the spenders of its outputs under a key for each sixteen of them" $+      withSystemTempDirectory "haskoin-store-times-" $ \dir ->+        openLayered net ctx dir $ \db -> do+          hist <- start layeredRules db+          -- forty outputs, of which three are spent, in three groups+          let wide = Tx 2 [TxIn (coinOf hist) "" maxBound] (replicate 40 (TxOut 1000 (p2pkh 1))) [] 0+              spend i = Tx 2 [TxIn (OutPoint (txHash wide) i) "" maxBound] [TxOut 500 (p2pkh 2)] [] 0+              spends = map spend [3, 17, 39]+              keysNow = do+                rows <- dump db (spenderCF db.db)+                return+                  [ (g, IntMap.keys m)+                  | (k, v) <- rows,+                    Right (SpenderKey h g) <- [decode k],+                    h == txHash wide,+                    Right (SpenderVal m) <- [decode v]+                  ]+              mine txs = do+                mined <- blockOn db txs >>= apply layeredRules db+                isRight mined.result `shouldBe` True+          mine [wide]+          written <- dump db (txCF db.db)+          keysNow `shouldReturn` []+          mine spends+          keysNow `shouldReturn` [(0, [3]), (1, [17]), (2, [39])]+          -- the record is as the first block wrote it+          now <- dump db (txCF db.db)+          [v | (k, v) <- now, k `elem` map fst written] `shouldBe` map snd written+          -- and the transaction is read with its spenders, each also by itself+          found <- L.withView db (`L.getTxData` txHash wide)+          let expected = IntMap.fromList [(fromIntegral i, Spender (txHash (spend i)) 0) | i <- [3, 17, 39]]+          fmap (.spenders) found `shouldBe` Just expected+          forM_ [3, 4, 17, 39] $ \i ->+            L.withView db (`L.getSpender` OutPoint (txHash wide) i)+              `shouldReturn` IntMap.lookup (fromIntegral i) expected+          -- a block that is reverted takes its spenders along, and a group+          -- with none left has no key+          best <- Q.getBestBlock db >>= maybe (fail "no best block") return+          reverted <- apply layeredRules db (Revert best)+          isRight reverted.result `shouldBe` True+          keysNow `shouldReturn` []+          Q.getChainSpenders db (txHash wide) `shouldReturn` IntMap.empty+          -- the transactions that spent them are back in the mempool, and+          -- the transaction is read with them as its spenders still+          fmap (.spenders) <$> L.withView db (`L.getTxData` txHash wide) `shouldReturn` Just expected+    modifyMaxSuccess (const 20) $+      it "has the spenders of its outputs moved to keys of their own by the migration" $+        -- A database as the layered rules write it has a key for each+        -- spent output. The spenders are put where version 21 had them,+        -- in the record of the transaction that made the output, the keys+        -- are deleted, and the database is marked as of that version. The+        -- migration must give back the records and the keys as they were.+        property . forAll (vectorOf 40 anyNumber) $ \seeds -> ioProperty $+          withSystemTempDirectory "haskoin-store-times-" $ \dir ->+            openLayered net ctx dir $ \db -> withShadow net ctx $ \shadow -> do+              hist0 <- start layeredRules db+              void (foldM (walk shadow db) hist0 seeds)+              records <- dump db (txCF db.db)+              keys <- dump db (spenderCF db.db)+              let spent =+                    [ (h, m :: IntMap.IntMap Spender)+                    | (k, v) <- keys,+                      Right (SpenderKey h _) <- [decode k],+                      Right (SpenderVal m) <- [decode v]+                    ]+                  inside td = IntMap.unions [m | (h, m) <- spent, h == txHash td.tx]+                  old (k, v) = case decode v of+                    Right td | inChain td -> (k, encode (embed td (inside td)))+                    _ -> (k, v)+              writeBatch db.db $+                insertOp VersionKey layeredVersion+                  : [DelCF (spenderCF db.db) k | (k, _) <- keys]+                    <> [PutCF (txCF db.db) k v | (k, v) <- map old records]+              done <- upgradeDatabase (\_ -> return ()) db+              -- every record that had a spender in it was written again+              done.rewritten `shouldBe` length [() | (k, v) <- records, old (k, v) /= (k, v)]+              dump db (txCF db.db) `shouldReturn` records+              dump db (spenderCF db.db) `shouldReturn` keys+    it "is set by a migration that carries on after it was interrupted" $+      withSystemTempDirectory "haskoin-store-times-" $ \dir ->+        openLayered net ctx dir $ \db -> withShadow net ctx $ \shadow -> do+          hist0 <- start layeredRules db+          -- payments, and then blocks, which confirm some of them+          void (foldM (walk shadow db) hist0 (onlyPayments [1 .. 6] <> [8 .. 11]))+          original <- dump db (txCF db.db)+          let confirmed = [(k, td :: TxData) | (k, v) <- original, Right td <- [decode v], inChain td]+          length confirmed `shouldSatisfy` (> 0)+          -- as a migration leaves it that stopped before its first change+          writeBatch db.db $+            [insertOp VersionKey currentVersion, insertOp MigrationKey layeredVersion]+              <> [PutCF (txCF db.db) k (encode ((td :: TxData) {timestamp = 7})) | (k, td) <- confirmed]+          said <- newIORef []+          done <- upgradeDatabase (\line -> modifyIORef' said (line :)) db+          done.rewritten `shouldBe` length confirmed+          dump db (txCF db.db) `shouldReturn` original+          (retrieve db.db MigrationKey :: IO (Maybe Word32)) `shouldReturn` Nothing+          readIORef said >>= (`shouldSatisfy` any ("Carrying on" `T.isPrefixOf`))+    it "is not guessed for a transaction whose block the database does not have" $+      withSystemTempDirectory "haskoin-store-times-" $ \dir ->+        openLayered net ctx dir $ \db -> do+          _ <- start layeredRules db+          rows <- dump db (txCF db.db)+          let confirmed = [(k, td :: TxData) | (k, v) <- rows, Right td <- [decode v], inChain td]+          (k, td) <- maybe (fail "no confirmed transaction") return (listToMaybe confirmed)+          writeBatch+            db.db+            [ insertOp VersionKey layeredVersion,+              PutCF (txCF db.db) k (encode ((td :: TxData) {block = BlockRef 5000 0}))+            ]+          upgradeDatabase (\_ -> return ()) db `shouldThrow` \case+            Inconsistent _ -> True+            _ -> False++net :: Network+net = btcRegTest++-- | Choose one operation of a history from a database of version 20 that+-- has gone through it ('withShadow'), and carry it out there and, under+-- the layered rules, in a database of the layered layout.+walk :: DatabaseReader -> DatabaseReader -> History -> Word16 -> IO History+walk shadow db hist seed =+  plan shadow hist seed >>= \case+    (hist', Nothing) -> return hist'+    (hist', Just op) -> do+      void (apply mixedRules shadow op)+      record db hist' op <$> apply layeredRules db op++-- | A transaction with a time that is not that of its block, and marked as+-- one that can be replaced, which a confirmed transaction cannot be.+later :: TxData -> TxData+later td = (td :: TxData) {timestamp = td.timestamp + 12345, rbf = True}++-- | Bring a database of version 21 to the version that the store runs on.+settle :: DatabaseReader -> IO ()+settle db = void (migrateRecords (\_ -> return ()) db)++-- | A transaction with spenders of its outputs in its record.+embed :: TxData -> IntMap.IntMap Spender -> TxData+embed td inside = (td :: TxData) {spenders = inside}++-- | Whether a transaction is in the chain.+inChain :: TxData -> Bool+inChain td = case td.block of+  BlockRef {} -> not td.deleted+  MemRef {} -> False++-- | The confirmed transactions of a database whose time is not that of+-- their block, each with both times.+wrongTimes :: DatabaseReader -> IO [(TxHash, Word64, Maybe Word64)]+wrongTimes db = do+  rows <- dump db (txCF db.db)+  fmap catMaybes . mapM check $ [td :: TxData | (_, v) <- rows, Right td <- [decode v], inChain td]+  where+    check td = do+      hashes <- Q.getBlocksAtHeight db td.block.height+      blocks <- catMaybes <$> mapM (Q.getBlock db) hashes+      let time = listToMaybe [fromIntegral b.header.timestamp | b <- blocks, b.main]+      return (if time == Just td.timestamp then Nothing else Just (txHash td.tx, td.timestamp, time))++data Interrupted = Interrupted+  deriving (Eq, Show, Exception)++-- | Make a database of version 20 in a directory: the genesis block, a block+-- with a coin, and whatever the function does next. Gives the history, and+-- the answers of the database to every query about it.+mixedDatabase ::+  Ctx ->+  FilePath ->+  (DatabaseReader -> History -> IO History) ->+  IO (History, Answers)+mixedDatabase ctx dir go =+  open20 net ctx dir $ \db -> do+    hist <- start mixedRules db >>= go db+    answers <- mixed db (ask hist)+    return (hist, answers)++-- | Add a transaction to the mempool, by the rules of today's layout.+addTx :: DatabaseReader -> History -> Tx -> IO History+addTx db hist tx = record db hist op <$> apply mixedRules db op+  where+    op = Pay tx hist.clock
+ test/Haskoin/Store/Database/Query/LayeredSpec.hs view
@@ -0,0 +1,173 @@+module Haskoin.Store.Database.Query.LayeredSpec (spec) where++import Control.Monad (foldM, forM_, void)+import Data.ByteString (ByteString)+import Data.Default (def)+import Data.List (sort)+import Haskoin+  ( Address,+    OutPoint (..),+    Tx (..),+    TxHash (..),+    TxIn (..),+    TxOut (..),+    bchRegTest,+    btcRegTest,+    prepareContext,+    scriptToAddressBS,+    sha256,+    txHash,+  )+import Haskoin.Store.Common (Limits (..), Start (..), StoreRead (..))+import Haskoin.Store.Data+  ( Balance (..),+    TxData (..),+    TxRef (..),+    Unspent (..),+    confirmed,+  )+import Haskoin.Store.Database.Migrate (upgradeDatabase)+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader (DatabaseReader (..))+import Haskoin.Store.Histories+import Haskoin.Store.TestBlocks (coinbaseTx, script)+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck (arbitrary, forAll, ioProperty, property, tabulate, vectorOf)+import Test.QuickCheck.Gen (unGen)+import Test.QuickCheck.Random (mkQCGen)+import UnliftIO (withSystemTempDirectory)++spec :: Spec+spec = prepareContext $ \ctx -> describe "queries of the layered layout" $ do+  forM_ [("Bitcoin", btcRegTest), ("Bitcoin Cash", bchRegTest)] $ \(name, net) ->+    modifyMaxSuccess (const 30) $+      it ("give the pages of today's queries on a migrated database, on " <> name) $+        property . forAll ((,,) <$> vectorOf 45 anyNumber <*> vectorOf 12 anyNumber <*> arbitrary) $+          \(seeds, last', seed) -> ioProperty $+            withSystemTempDirectory "haskoin-store-pages-" $ \dir -> do+              -- anything at first, then payments, so that the mempool has+              -- something; and questions about what came of it+              (asked, was, starts, fair) <- open20 net ctx dir $ \db -> do+                hist <- start mixedRules db >>= \hist0 -> foldM (step db) hist0 (seeds <> onlyPayments last')+                let asked = unGen (questions net ctx hist 150) (mkQCGen seed) 30+                was <- replies mixed db asked+                starts <- mixed db (\queries -> mapM (from queries) asked)+                -- pages that start at a deleted transaction are not+                -- compared: the layered layout does not know those+                fair <- mapM (startsKnown db) asked+                return (asked, was, starts, fair)+              now <- openLayered net ctx dir $ \db -> do+                void (upgradeDatabase (\_ -> return ()) db)+                replies layered db asked+              length now `shouldBe` length was+              let kept :: [x] -> [x]+                  kept xs = [x | (x, True) <- zip xs (fair <> fair)]+              take 2 (differences (kept (asked <> asked)) (kept was) (kept now)) `shouldBe` []+              let content reply = if size reply == 0 then "nothing in it" else "something in it"+                  pages = [place <> ", " <> content reply | (Just place, reply) <- zip starts was]+              return (tabulate "pages that start at" pages True)++  it "cut pages from a list that is part in the mempool and part in the chain" $+    withSystemTempDirectory "haskoin-store-pages-" $ \dir ->+      openLayered btcRegTest ctx dir $ \db -> do+        hist <- start layeredRules db+        -- a payment from the coin of the first block, with change to the+        -- miner, and a payment from that change, in the same second+        let one = Tx 2 [TxIn (coinOf hist) "" maxBound] [TxOut 3000 (p2pkh 1), TxOut 2000 script] [] 0+            two = Tx 2 [TxIn (OutPoint (txHash one) 1) "" maxBound] [TxOut 1500 (p2pkh 2)] [] 0+            coinbase = txHash (coinbaseTx 1)+            -- those of one second come in the order of their hashes+            unconfirmed = sort [txHash one, txHash two]+            never = TxHash (sha256 ("never made" :: ByteString))+            page limits = L.withView db $ \v -> map (.txid) <$> L.getAddressTxs v miner limits+            outputs = L.withView db $ \v -> map (.outpoint) <$> L.getAddressUnspents v miner def+            amounts = L.withView db $ \v -> fmap numbers <$> L.getBalance v miner+            numbers b = (b.confirmed, b.unconfirmed, b.utxo, b.txs, b.received)+            miner :: Address+            miner = either error id (scriptToAddressBS btcRegTest ctx script)+        forM_ [one, two] $ \tx ->+          (.result) <$> apply layeredRules db (Pay tx 1000) `shouldReturn` Right "True"+        page def `shouldReturn` unconfirmed <> [coinbase]+        page def {limit = 1} `shouldReturn` take 1 unconfirmed+        page def {limit = 2, offset = 1} `shouldReturn` drop 1 unconfirmed <> [coinbase]+        page def {offset = 2} `shouldReturn` [coinbase]+        -- from a mempool transaction: it, and what is older+        page def {start = Just (AtTx (unconfirmed !! 1))} `shouldReturn` drop 1 unconfirmed <> [coinbase]+        page def {start = Just (AtTx (unconfirmed !! 1)), offset = 1} `shouldReturn` [coinbase]+        -- from a confirmed transaction: nothing of the mempool+        page def {start = Just (AtTx coinbase)} `shouldReturn` [coinbase]+        -- from a height: nothing of the mempool either, even above the best+        -- block, as today+        page def {start = Just (AtBlock 5)} `shouldReturn` [coinbase]+        page def {start = Just (AtBlock 0)} `shouldReturn` []+        page def {start = Just (AtTx never)} `shouldReturn` []+        -- the mempool spends the coin, and the change that it made+        outputs `shouldReturn` []+        amounts `shouldReturn` Just (0, 0, 0, 3, 5000002000)+        -- a block confirms the first payment and not the second+        block <- blockOn db [one]+        (.removed) <$> apply layeredRules db block `shouldReturn` [txHash one]+        let reward = txHash (coinbaseTx 2)+        page def `shouldReturn` [txHash two, txHash one, reward, coinbase]+        page def {start = Just (AtTx (txHash one))} `shouldReturn` [txHash one, reward, coinbase]+        page def {start = Just (AtBlock 2), offset = 1, limit = 1} `shouldReturn` [reward]+        -- the change is in the chain now, and the mempool still spends it+        outputs `shouldReturn` [OutPoint reward 0]+        amounts `shouldReturn` Just (5000000000, 0, 1, 4, 10000002000)++  it "answer from one moment, whatever is written in the meantime" $+    withSystemTempDirectory "haskoin-store-pages-" $ \dir ->+      openLayered btcRegTest ctx dir $ \db -> do+        hist0 <- start layeredRules db+        let chained = payments (coinOf hist0)+            pay hist tx = record db hist (Pay tx hist.clock) <$> apply layeredRules db (Pay tx hist.clock)+        hist <- foldM pay hist0 (take 4 chained)+        let asked = unGen (questions btcRegTest ctx hist 100) (mkQCGen 7) 30+            everything queries = (,) <$> ask hist queries <*> mapM (answer queries) asked+        (early, during) <- L.withView db $ \v -> do+          early <- everything (L.layeredRead v)+          -- a block confirms two of the payments, and two more arrive+          block <- blockOn db (take 2 chained)+          mined <- apply layeredRules db block+          length mined.removed `shouldBe` 2+          forM_ (take 2 (drop 4 chained)) $ \tx ->+            (.result) <$> apply layeredRules db (Pay tx 2000) `shouldReturn` Right "True"+          during <- everything (L.layeredRead v)+          return (early, during)+        during `shouldBe` early+        -- a view taken now sees all of it+        late <- layered db everything+        length (fst late).mempool `shouldBe` 4+        (fst late).mempool `shouldNotBe` (fst early).mempool+        (fst late).balances `shouldNotBe` (fst early).balances+        take 1 (differences asked (snd early) (snd late)) `shouldNotBe` []++-- | The answers of a database to some questions: asked with the gaps that+-- the tests open their databases with, which reach every address of the+-- wallet that is paid, and asked again with small gaps, under which the+-- later ones are found only when earlier ones are used.+replies ::+  (DatabaseReader -> (StoreRead -> IO [Reply]) -> IO [Reply]) ->+  DatabaseReader ->+  [Question] ->+  IO [Reply]+replies with db asked = concat <$> mapM under [(db.initGap, db.maxGap), (2, 3)]+  where+    under (initial, gap) =+      with (db :: DatabaseReader) {initGap = initial, maxGap = gap} $ \queries ->+        mapM (answer queries) asked++-- | Where the page of a question starts, for the questions that have a+-- page, given the queries of today's layout.+from :: StoreRead -> Question -> IO (Maybe String)+from queries question = case startOf question of+  Nothing -> return Nothing+  Just Nothing -> return (Just "the top")+  Just (Just (AtBlock _)) -> return (Just "a height")+  Just (Just (AtTx h)) -> Just . maybe "a transaction that is not known" known <$> queries.getTxData h+  where+    known td+      | td.deleted = "a deleted transaction"+      | confirmed td.block = "a confirmed transaction"+      | otherwise = "a mempool transaction"
+ test/Haskoin/Store/Database/ViewsSpec.hs view
@@ -0,0 +1,67 @@+module Haskoin.Store.Database.ViewsSpec (spec) where++import Control.Monad (void)+import Data.List (sort)+import Haskoin (BlockHash, btcRegTest, prepareContext)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Views+import Haskoin.Store.Histories+import Test.Hspec+import UnliftIO (withSystemTempDirectory)++spec :: Spec+spec = prepareContext $ \ctx -> describe "views of the database by block" $ do+  let withChain go =+        withSystemTempDirectory "haskoin-store-views-" $ \dir ->+          open20 btcRegTest ctx dir $ \db -> do+            void (start mixedRules db)+            views <- newViews db+            let best = Q.getBestBlock db >>= maybe (fail "no best block") return+                mine = do+                  block <- blockOn db []+                  void (apply mixedRules db block)+                  b <- best+                  takeView views b+                  return b+            go db views best mine `shouldReturn` ()+            closeViews views++  it "answer from the block that each was taken at, whatever came after" $+    withChain $ \db views best mine -> do+      first <- best+      takeView views first+      second <- mine+      third <- mine+      sort <$> heldViews views `shouldReturn` sort [first, second, third]+      Q.getBestBlock db `shouldReturn` Just third+      withViewAt views first Q.getBestBlock `shouldReturn` Just (Just first)+      withViewAt views second Q.getBestBlock `shouldReturn` Just (Just second)+      -- the second block was not there yet when the first view was taken+      (fmap (fmap (const ())) <$> withViewAt views first (`Q.getBlock` second)) `shouldReturn` Just Nothing++  it "let go of the views that are not to be kept" $+    withChain $ \_ views best mine -> do+      first <- best+      takeView views first+      second <- mine+      keepViews views [second]+      heldViews views `shouldReturn` [second]+      withViewAt views first Q.getBestBlock `shouldReturn` (Nothing :: Maybe (Maybe BlockHash))+      withViewAt views second Q.getBestBlock `shouldReturn` Just (Just second)++  it "keep a view that is being read until its reader is done" $+    withChain $ \_ views best mine -> do+      first <- best+      takeView views first+      second <- mine+      result <- withViewAt views first $ \old -> do+        keepViews views [second]+        -- gone for anyone who asks now+        heldViews views `shouldReturn` [second]+        withViewAt views first Q.getBestBlock `shouldReturn` (Nothing :: Maybe (Maybe BlockHash))+        -- and still there for the one that was reading, while blocks go on+        third <- mine+        (,third) <$> Q.getBestBlock old+      fmap fst result `shouldBe` Just (Just first)+      -- the reader is done: the view is gone, and the others are as they were+      sort <$> heldViews views `shouldReturn` sort (second : maybe [] (pure . snd) result)
+ test/Haskoin/Store/Fleet/MemberSpec.hs view
@@ -0,0 +1,312 @@+module Haskoin.Store.Fleet.MemberSpec (spec) where++import Control.Monad (foldM_, void, when)+import Data.Default (def)+import Data.Either (isRight)+import Data.IORef (modifyIORef', newIORef, readIORef)+import Data.Serialize (encode)+import Data.Text qualified as T+import Data.Word (Word16)+import Haskoin (btcRegTest, prepareContext, txHash)+import Haskoin.Node (LogLevel (..))+import Haskoin.Store.Common (Limits (..), Start (..), StoreRead)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered (From (..))+import Haskoin.Store.Database.Query.Pooled (pooledRead)+import Haskoin.Store.Database.Views (closeViews)+import Haskoin.Store.Fleet.Member+import Haskoin.Store.Fleet.Redis (FleetKeys (..), poolWithdraw)+import Haskoin.Store.Fleet.RedisSpec (withRedis)+import Haskoin.Store.Fleet.XPub+import Haskoin.Store.Histories+import Haskoin.Store.Integration.Podman (podmanUsable)+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck (forAll, ioProperty, property, sized, vectorOf)+import UnliftIO (withSystemTempDirectory)++spec :: Spec+spec = prepareContext $ \ctx -> do+  usable <- runIO podmanUsable+  describe "member of a fleet" $ case usable of+    Left why -> it "answers as the store does when it is the whole fleet" (pendingWith why)+    Right () -> aroundAll withRedis $ do+      modifyMaxSuccess (const 20) $+        it "answers as the store does when it is the whole fleet" $ \(conn, fresh) ->+          -- A store alone in its fleet, through a random history of+          -- payments, replacements, blocks and reorganisations. The+          -- history is chosen from a database of the layout of version 20+          -- that goes through the same, since that is what 'plan' reads. After each+          -- step it takes its turn as a member, and then every answer that+          -- it gives from the view at the fleet tip with the pool of Redis+          -- on top must be the answer that it gives from its own database.+          property . forAll (vectorOf 60 anyNumber) $ \seeds -> ioProperty $+            withSystemTempDirectory "haskoin-store-member-" $ \dir ->+              openLayered btcRegTest ctx dir $ \db -> withShadow btcRegTest ctx $ \shadow -> do+                keys <- fresh+                let config =+                      MemberConfig+                        { prefix = keys.prefix,+                          name = "a",+                          lease = 60000,+                          depth = 6,+                          expiry = 0,+                          cacheMax = 0,+                          cacheMinAddrs = 0,+                          cacheMinTxs = 0,+                          stats = Nothing,+                          note = \_ _ -> return ()+                        }+                    best = Q.getBestBlock db >>= maybe (fail "no best block") return+                m <- newMember config conn db+                hist0 <- start layeredRules db+                best >>= noteBest m+                memberTick m+                let turn hist seed =+                      plan shadow hist seed >>= \case+                        (hist', Nothing) -> return hist'+                        (hist', Just op) -> do+                          void (apply mixedRules shadow op)+                          out <- apply layeredRules db op+                          let hist'' = record db hist' op out+                          -- the store tells its member what it deletes+                          memberForget m out.removed+                          when (isRight out.result) $ case op of+                            Mine _ _ -> best >>= noteBest m+                            Revert _ -> best >>= noteBest m+                            -- a store deletes what is too old, and the pool+                            -- does the same by its own clock+                            Delete _ -> void (poolWithdraw conn keys 32 (map encode out.removed))+                            Pay _ _ -> return ()+                          memberTick m+                          own <- layered db (ask hist'')+                          answering m (\r pool -> ask hist'' (pooledRead r pool)) >>= \case+                            Answered shared -> (show op, shared) `shouldBe` (show op, own)+                            Alone -> expectationFailure "the fleet has no tip"+                            Behind -> expectationFailure "the store has no view of the fleet tip"+                          return hist''+                foldM_ turn hist0 seeds+                closeViews m.views+      modifyMaxSuccess (const 20) $+        it "answers about a wallet from the cache as it does from the database" $ \(conn, fresh) ->+          -- The same store and histories, with the cache of extended+          -- public keys on. After each step, every question about the+          -- wallet of the histories is asked of the view at the fleet tip+          -- with the pool on top, and again with the cache in front: the+          -- answers must be the same, whether the cache has the wallet,+          -- has it with other addresses, or does not have it. The wallet+          -- is cached after some steps and not after others, so that+          -- blocks and payments find it in the cache and out of it. Right+          -- after it is cached, the cache must be what answers, and only+          -- to a caller that has the wallet with as many addresses.+          --+          -- At one step in five the fleet tip is moved by the same store+          -- taking its turn without the cache, as a store does that stops+          -- between moving the tip and bringing the cache along. The+          -- cache is then at another block than the tip, and must not+          -- answer.+          --+          -- The first history is always 'leftAndBack'.+          let histories = sized (\n -> if n == 0 then return leftAndBack else vectorOf 60 anyNumber)+           in property . forAll histories $ \seeds -> ioProperty $+                withSystemTempDirectory "haskoin-store-member-" $ \dir ->+                  openLayered btcRegTest ctx dir $ \db -> withShadow btcRegTest ctx $ \shadow -> do+                    keys <- fresh+                    said <- newIORef []+                    let config =+                          MemberConfig+                            { prefix = keys.prefix,+                              name = "a",+                              lease = 60000,+                              depth = 6,+                              expiry = 0,+                              cacheMax = 10,+                              cacheMinAddrs = 0,+                              cacheMinTxs = 0,+                              stats = Nothing,+                              note = \level line -> modifyIORef' said ((level, line) :)+                            }+                        best = Q.getBestBlock db >>= maybe (fail "no best block") return+                        key = wallet ctx+                    m <- newMember config conn db+                    -- the same store, as it takes its turns without the cache+                    bare <- newMember (config {cacheMax = 0} :: MemberConfig) conn db+                    hits <- newIORef (0 :: Int)+                    hist0 <- start layeredRules db+                    best >>= \b -> noteBest m b >> noteBest bare b+                    memberTick m+                    let tipNow = memberTip m >>= maybe (fail "the fleet has no tip") return+                        inFleet :: (StoreRead -> StoreRead -> IO a) -> IO a+                        inFleet action = do+                          tip <- tipNow+                          answeringCached m tip (\plain cached _ -> action plain cached)+                            >>= maybe (fail "the store has no view of the fleet tip") return+                        turn hist seed =+                          plan shadow hist seed >>= \case+                            (hist', Nothing) -> return hist'+                            (hist', Just op) -> do+                              void (apply mixedRules shadow op)+                              out <- apply layeredRules db op+                              let hist'' = record db hist' op out+                              memberForget m out.removed+                              memberForget bare out.removed+                              when (isRight out.result) $ case op of+                                Mine _ _ -> best >>= \b -> noteBest m b >> noteBest bare b+                                Revert _ -> best >>= \b -> noteBest m b >> noteBest bare b+                                Delete _ -> void (poolWithdraw conn keys 32 (map encode out.removed))+                                Pay _ _ -> return ()+                              if seed `mod` 5 == 0+                                then do+                                  -- the tip moves, if the step made a block,+                                  -- and the cache stays where it was: not in+                                  -- every history, but in most+                                  memberTick bare+                                  -- the member with the cache learns the tip+                                  memberTick m+                                else memberTick m+                              let made = map txHash hist''.made+                                  pick xs = xs !! (fromIntegral seed `mod` length xs)+                                  starts =+                                    [Nothing, Just (AtBlock (fromIntegral seed `mod` 8))]+                                      <> [Just (AtTx (pick made)) | not (null made)]+                                  limits =+                                    [ Limits {limit, offset, start = from}+                                    | from <- starts,+                                      (limit, offset) <- [(0, 0), (2, 0), (3, fromIntegral seed `mod` 3)]+                                    ]+                                  asked =+                                    [WalletBalances, WalletCount]+                                      <> map WalletTxs limits+                                      <> map WalletUnspents limits+                              (direct, cached) <- inFleet $ \plain cached ->+                                (,) <$> mapM (answer plain) asked <*> mapM (answer cached) asked+                              (show op, differences asked direct cached) `shouldBe` (show op, [])+                              -- cache the wallet after two steps of three+                              when (seed `mod` 3 /= 0) $ do+                                tip <- tipNow+                                built <- answeringAt m tip (\r pool _ -> cacheBuild conn keys tip r pool 1 key)+                                at <- cacheTip conn keys+                                if at /= Just tip+                                  then do+                                    -- the cache is behind the tip: nothing is+                                    -- written to it, and it does not answer+                                    built `shouldSatisfy` (`elem` [Just Abandoned, Just (Built 0)])+                                    cachedEntry conn keys tip 20 32 1 key `shouldReturn` Miss+                                  else do+                                    built `shouldSatisfy` \case+                                      Just (Built _) -> True+                                      _ -> False+                                    cachedEntry conn keys tip 20 32 1 key >>= \case+                                      Hit e -> do+                                        modifyIORef' hits (+ 1)+                                        let (n0, n1) = e.sizes+                                            isHit = \case+                                              Hit _ -> True+                                              _ -> False+                                        cachedTxs conn keys tip key (n0, n1) Top def >>= (`shouldSatisfy` isHit)+                                        cachedCount conn keys tip key (n0, n1) >>= (`shouldSatisfy` isHit)+                                        -- not for a caller with other addresses+                                        cachedTxs conn keys tip key (n0 + 1, n1) Top def `shouldReturn` Miss+                                        cachedCount conn keys tip key (n0, n1 + 1) `shouldReturn` Miss+                                        let more = e {sizes = (n0 + 1, n1)}+                                        cachedUnspents conn keys tip key more Top def `shouldReturn` Miss+                                      other -> expectationFailure ("the cache did not answer: " <> show other)+                                (direct', cached') <- inFleet $ \plain through ->+                                  (,) <$> mapM (answer plain) asked <*> mapM (answer through) asked+                                (show op, differences asked direct' cached') `shouldBe` (show op, [])+                              return hist''+                    foldM_ turn hist0 seeds+                    -- the least recently used entries go when there are too+                    -- many, and nothing of them is left+                    memberTick m+                    memberTick m+                    tip <- tipNow+                    cacheTip conn keys `shouldReturn` Just tip+                    _ <- answeringAt m tip (\r pool _ -> cacheBuild conn keys tip r pool 1 key)+                    -- its size is one entry, with the addresses of the wallet+                    (n0, n1) <- cacheHave conn keys key+                    cacheSize conn keys `shouldReturn` (1, n0 + n1)+                    cacheEvict conn keys 1 `shouldReturn` 0+                    cacheEvict conn keys 0 `shouldReturn` 1+                    cacheSize conn keys `shouldReturn` (0, 0)+                    cachedEntry conn keys tip 20 32 1 key `shouldReturn` Miss+                    cacheHave conn keys key `shouldReturn` (0, 0)+                    closeViews m.views+                    closeViews bare.views+                    -- what the member logged: that it emptied the cache, and+                    -- why, as information, and nothing else above debugging+                    logged <- readIORef said+                    let emptied = [line | (LevelInfo, line) <- logged]+                    [() | (level, _) <- logged, level /= LevelInfo, level /= LevelDebug] `shouldBe` []+                    emptied `shouldSatisfy` all ("Emptied the xpub cache at block " `T.isPrefixOf`)+                    readIORef hits >>= (`shouldSatisfy` (> 10))++-- | A history that a run of the tests found: a block is mined, reverted and+-- mined again while the cache is not brought along, so that the fleet tip+-- comes back to the block that the cache was at. The cache then looked+-- current, with entries that the pool had taken the transactions of that+-- block out of on the way. It is why the cache is at a move of the tip and+-- not at a block.+leftAndBack :: [Word16]+leftAndBack =+  [ 2944,+    33599,+    29992,+    62497,+    57974,+    57422,+    44271,+    60202,+    57257,+    19310,+    53832,+    39324,+    44849,+    28951,+    37975,+    2064,+    15159,+    34699,+    16471,+    3642,+    14550,+    867,+    22752,+    14436,+    39637,+    398,+    25449,+    11005,+    20967,+    47441,+    46443,+    63224,+    4787,+    36990,+    56953,+    1543,+    36370,+    24815,+    19050,+    39880,+    16054,+    22273,+    62670,+    13555,+    30149,+    1048,+    1444,+    45704,+    39517,+    1970,+    43989,+    15518,+    54685,+    58539,+    63687,+    1789,+    3783,+    27016,+    43138,+    28658+  ]
+ test/Haskoin/Store/Fleet/PoolSpec.hs view
@@ -0,0 +1,182 @@+module Haskoin.Store.Fleet.PoolSpec (spec) where++import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Set (Set)+import Data.Set qualified as Set+import Haskoin.Store.Fleet.Pool+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck++-- | An output: the transaction that made it, and which of its two outputs.+type Out = (Int, Int)++-- | A chain, newest block first, each block its coinbase and then its+-- other transactions; what every transaction ever made spends; the shared+-- pool; and the number of the next transaction.+data World = World+  { chain :: ![[Int]],+    inputs :: !(Map Int [Out]),+    pool :: !(Pool Int Out),+    fresh :: !Int+  }+  deriving (Show)++-- | Something that happens, with a number to choose the details from.+data Op+  = -- | a store offers a transaction, which may spend what is not there or+    -- what the pool spends, naming what it replaces or not+    Offer !Int+  | -- | a store withdraws a transaction+    Withdraw !Int+  | -- | the tip moves to a new block, with some of the pool and perhaps a+    -- transaction that the pool has not seen+    Mine !Int+  | -- | the tip moves back a block+    Unwind+  deriving (Show)++instance Arbitrary Op where+  arbitrary =+    frequency+      [ (8, Offer <$> choose (0, 100000)),+        (1, Withdraw <$> choose (0, 100000)),+        (3, Mine <$> choose (0, 100000)),+        (1, pure Unwind)+      ]++start :: World+start = World {chain = [[0]], inputs = Map.singleton 0 [], pool = empty, fresh = 1}++-- | The outputs of the chain that no transaction of the chain spends.+unspent :: World -> Set Out+unspent w = made `Set.difference` used+  where+    txs = concat w.chain+    made = Set.fromList [(t, i) | t <- txs, i <- [0, 1]]+    used = Set.fromList (concatMap (\t -> Map.findWithDefault [] t w.inputs) txs)++pick :: Int -> [a] -> [a]+pick _ [] = []+pick n xs = [xs !! (n `mod` length xs)]++step :: World -> Op -> World+step w = \case+  Offer n ->+    let t = w.fresh+        chainOuts = Set.toList (unspent w)+        poolOuts = [(p, i) | p <- Map.keys w.pool.spends, i <- [0, 1]]+        -- now and then an output that never was, or one long spent+        odd' = [(t + 7, 0), (0, 0)]+        from = case n `mod` 10 of+          0 -> odd'+          k | k < 6 -> chainOuts+          _ -> poolOuts <> chainOuts+        spent = Set.toList (Set.fromList (pick n from <> pick (n `div` 7) (chainOuts <> poolOuts)))+        rivals = [x | o <- spent, Just x <- [Map.lookup o w.pool.spender]]+        replaced = if even (n `div` 3) then rivals else []+        w' = w {fresh = t + 1, inputs = Map.insert t spent w.inputs}+     in case offer fst (`Set.member` unspent w) t spent replaced w.pool of+          Taken p -> w' {pool = p}+          Refused _ -> w'+  Withdraw n -> w {pool = withdraw (pick n (Map.keys w.pool.spends)) w.pool}+  Mine n ->+    let coinbase = w.fresh+        free = unspent w+        -- transactions of the pool whose parents are in the chain or picked+        -- before them, as a block needs+        ready picked t = all (\o -> Set.member o free || fst o `elem` picked) (Map.findWithDefault [] t w.pool.spends)+        close picked =+          case [t | t <- Map.keys w.pool.spends, t `notElem` picked, ready picked t] of+            t : _ | length picked < n `mod` 5 -> close (picked <> [t])+            _ -> picked+        fromPool = close []+        -- and one that the pool never saw, which may spend what it spends+        taken = concatMap (\t -> Map.findWithDefault [] t w.pool.spends) fromPool+        unseen = [(coinbase + 1, pick (n `div` 5) [o | o <- Set.toList free, o `notElem` taken]) | even n]+        unseen' = [(t, os) | (t, os) <- unseen, not (null os)]+        block = coinbase : fromPool <> map fst unseen'+        ins = foldr (uncurry Map.insert) (Map.insert coinbase [] w.inputs) unseen'+        change =+          Change+            { returned = [],+              gone = [],+              confirmed = block,+              spent = concatMap (\t -> Map.findWithDefault [] t ins) block+            }+     in w {chain = block : w.chain, inputs = ins, fresh = coinbase + 2, pool = advance fst change w.pool}+  Unwind -> case w.chain of+    (coinbase : txs) : rest@(_ : _) ->+      let change =+            Change+              { returned = [(t, Map.findWithDefault [] t w.inputs) | t <- txs],+                gone = [coinbase],+                confirmed = [],+                spent = []+              }+       in w {chain = rest, pool = advance fst change w.pool}+    _ -> w++-- | What is wrong with the pool of a world, if anything.+wrong :: World -> [String]+wrong w =+  [ "transaction " <> show t <> " spends " <> show o <> ", which nothing made or something spent"+  | (t, os) <- Map.toList w.pool.spends,+    o <- os,+    not (member (fst o) w.pool),+    not (Set.member o free)+  ]+    <> ["transaction " <> show t <> " is in a block and in the pool" | t <- concat w.chain, member t w.pool]+    <> ["two transactions spend one output" | Map.size spenders /= length (concat (Map.elems w.pool.spends))]+    <> ["the index of spenders is off" | spenders /= w.pool.spender]+    <> ["the index of children is off" | kids /= w.pool.children]+  where+    free = unspent w+    spenders = Map.fromList [(o, t) | (t, os) <- Map.toList w.pool.spends, o <- os]+    kids = Map.fromListWith Set.union [(fst o, Set.singleton t) | (t, os) <- Map.toList w.pool.spends, o <- os]++spec :: Spec+spec = describe "shared mempool" $ do+  let a = 10 :: Int+      b = 11 :: Int+      c = 12 :: Int+      onChain o = fst o == (0 :: Int)+      taken = \case+        Taken p -> p+        Refused why -> error (show why)+      one = taken (offer fst onChain a [(0, 0)] [] (empty :: Pool Int Out))+      two = taken (offer fst onChain b [(a, 0)] [] one)++  it "keeps the first of two that spend the same output, unless it is replaced" $ do+    offer fst onChain c [(0, 0)] [] two `shouldBe` Refused (Conflict [a])+    -- replacing the first takes what spends it along+    Map.keys (taken (offer fst onChain c [(0, 0)] [a] two)).spends `shouldBe` [c]++  it "refuses what spends an output that is neither in the pool nor unspent at the tip" $ do+    offer fst onChain c [(5, 0)] [] two `shouldBe` Refused (Orphan [(5, 0)])+    offer fst onChain a [(0, 0)] [] two `shouldBe` Refused Known++  it "lets a confirmed transaction go alone, and a conflict with all that spends it" $ do+    let confirmedA = Change {returned = [], gone = [], confirmed = [a], spent = [(0, 0)]}+        rival = Change {returned = [], gone = [], confirmed = [c], spent = [(0, 0)]}+    Map.keys (advance fst confirmedA two).spends `shouldBe` [b]+    advance fst rival two `shouldBe` empty++  it "takes back what an abandoned block had confirmed, with what spends it" $ do+    let forward = Change {returned = [], gone = [], confirmed = [a], spent = [(0, 0)]}+        back = Change {returned = [(a, [(0, 0)])], gone = [], confirmed = [], spent = []}+        undone = advance fst back (advance fst forward two)+    undone `shouldBe` two+    -- the transaction is the parent of the other undone+    withdraw [a] undone `shouldBe` empty++  modifyMaxSuccess (const 3000) $ do+    it "stays valid on top of the tip, whatever is offered, mined and unwound" $+      property $ \ops ->+        let worlds = scanl step start ops+            trouble = [(i, problems) | (i, w) <- zip [0 :: Int ..] worlds, let problems = wrong w, not (null problems)]+            final = last worlds+         in counterexample (show (take 1 trouble)) $+              cover 20 (Map.size final.pool.spends >= 3) "three or more in the pool at the end" $+                cover 20 (length final.chain >= 3) "three or more blocks at the end" (null trouble)
+ test/Haskoin/Store/Fleet/RedisSpec.hs view
@@ -0,0 +1,423 @@+module Haskoin.Store.Fleet.RedisSpec (spec, withRedis) where++import Control.Monad (foldM_, forM_)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Char8 qualified as C+import Data.Containers.ListUtils (nubOrd, nubOrdOn)+import Data.IORef (atomicModifyIORef', newIORef)+import Data.List (sort, sortOn)+import Data.Map.Strict qualified as Map+import Data.Ord (Down (..))+import Data.Serialize (decode, encode)+import Data.Set qualified as Set+import Data.Word (Word32, Word64)+import Database.Redis qualified as Redis+import Haskoin.Store.Fleet (Time, empty, live, mayReport, move, report)+import Haskoin.Store.Fleet qualified as F+import Haskoin.Store.Fleet.Pool qualified as P+import Haskoin.Store.Fleet.Redis+import Haskoin.Store.Integration.Image (testImage)+import Haskoin.Store.Integration.Podman (hostPort, podmanUsable, withContainer)+import Haskoin.Store.Integration.Valkey (redisContainer, waitRedis)+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck++-- | A step of a fleet of three stores with blocks numbered from 0 to 9.+data Step+  = -- | a store reports some blocks, having views of some+    Report !Int ![Word32] ![Word32]+  | -- | a store tries to move the tip: from the tip as it is, or from+    -- another block, as one that was beaten to it would+    Shift !(Maybe Word32)+  | -- | milliseconds pass+    Wait !Time+  deriving (Show)++instance Arbitrary Step where+  arbitrary =+    frequency+      [ (5, Report <$> choose (0, 2) <*> blocks <*> blocks),+        (4, pure (Shift Nothing)),+        (1, Shift . Just <$> choose (0, 9)),+        (2, Wait <$> choose (1, 6))+      ]+    where+      blocks = choose (0, 5) >>= \n -> vectorOf n (choose (0, 9))++spec :: Spec+spec = do+  usable <- runIO podmanUsable+  describe "fleet tip in Redis" $ case usable of+    Left why -> it "does what the rules say" (pendingWith why)+    Right () -> aroundAll withRedis $ do+      modifyMaxSuccess (const 300) $+        it "does what the rules say, step by step" $ \(conn, fresh) ->+          property $ \steps -> ioProperty $ do+            keys <- fresh+            foldM_ (check conn keys) (empty :: F.Fleet ByteString ByteString, 1000) (steps :: [Step])+      it "counts leases on the clock of Redis when given no other" $ \(conn, fresh) -> do+        keys <- fresh+        fleetReport conn keys Nothing 60000 "a" [block 2, block 1] [] `shouldReturn` True+        fleetMove conn keys Nothing Nothing `shouldReturn` MovedTo (block 2)+        fleetTip conn keys `shouldReturn` Just (block 2)+        -- a store without a view of the tip is kept out while the first is live+        fleetReport conn keys Nothing 60000 "b" [block 1] [block 1] `shouldReturn` False+        fleetReport conn keys Nothing 60000 "b" [block 2] [block 2] `shouldReturn` True+        map fst <$> fleetLive conn keys `shouldReturn` ["a", "b"]+      modifyMaxSuccess (const 300) $+        it "keeps the shared mempool as the rules say, step by step" $ \(conn, fresh) ->+          property $ \steps -> ioProperty $ do+            keys <- fresh+            foldM_ (pooled conn keys) (P.empty, Nothing, 0) (steps :: [PoolStep])+      it "turns away an offer that was checked against another tip" $ \(conn, fresh) -> do+        keys <- fresh+        fleetReport conn keys Nothing 60000 "a" [block 1] [] `shouldReturn` True+        fleetMove conn keys Nothing Nothing `shouldReturn` MovedTo (block 1)+        let seven = (txOf 7 [out 0 0]) {body = "seven"}+        poolOffer conn keys 4 Nothing seven [True] [] `shouldReturn` Late+        poolOffer conn keys 4 (Just (block 1)) seven [True] [] `shouldReturn` Taken+        poolBody conn keys (block 7) `shouldReturn` Just "seven"+        -- the tip does not move to a block that the stores do not agree on+        fleetAdvance conn keys 4 Nothing (Just (block 1)) (block 2) (Just nothing) [] `shouldReturn` Raced+        fleetReport conn keys Nothing 60000 "a" [block 2, block 1] [block 1] `shouldReturn` True+        let confirms = nothing {confirmed = [block 7], spent = [out 0 0]}+        fleetAdvance conn keys 4 Nothing (Just (block 1)) (block 2) (Just confirms) [] `shouldReturn` MovedTo (block 2)+        poolSpends conn keys `shouldReturn` []+        poolBody conn keys (block 7) `shouldReturn` Nothing+      it "tells what happens to the pool and to the tip, in the order in which it happens" $ \(conn, fresh) -> do+        keys <- fresh+        fleetReport conn keys Nothing 60000 "a" [block 1] [] `shouldReturn` True+        fleetAdvance conn keys 4 Nothing Nothing (block 1) (Just nothing) [] `shouldReturn` MovedTo (block 1)+        -- a store that starts to listen hears what comes after+        start <- fleetEventsEnd conn keys+        let seven = txOf 7 [out 0 0]+            eight = txOf 8 [out 7 0]+            nine = txOf 9 [out 0 1]+        forM_ [seven, eight, nine] $ \t ->+          poolOffer conn keys 4 (Just (block 1)) t [True] [] `shouldReturn` Taken+        -- what spends a transaction that is withdrawn leaves with it+        _ <- poolWithdraw conn keys 4 [block 7]+        -- a block confirms the third, which is not an event of the mempool,+        -- and the tip moves through two blocks+        fleetReport conn keys Nothing 60000 "a" [block 3, block 2, block 1] [block 1] `shouldReturn` True+        let confirms = nothing {confirmed = [block 9], spent = [out 0 1]}+        fleetAdvance conn keys 4 Nothing (Just (block 1)) (block 3) (Just confirms) [block 2, block 3]+          `shouldReturn` MovedTo (block 3)+        heard <- map snd <$> fleetEventsAfter conn keys start 10+        take 3 heard `shouldBe` map FleetEntered [block 7, block 8, block 9]+        sort (take 2 (drop 3 heard)) `shouldBe` map FleetLeft [block 7, block 8]+        drop 5 heard `shouldBe` map FleetBlock [block 2, block 3]+        -- and nothing twice: from the last event on there is nothing+        end <- fleetEventsEnd conn keys+        fleetEventsAfter conn keys end 10 `shouldReturn` []+      it "empties the pool for a store that is alone and does not know the way" $ \(conn, fresh) -> do+        keys <- fresh+        fleetReport conn keys (Just 0) 10 "a" [block 1] [] `shouldReturn` True+        fleetAdvance conn keys 4 (Just 0) Nothing (block 1) Nothing [] `shouldReturn` MovedTo (block 1)+        poolOffer conn keys 4 (Just (block 1)) (txOf 7 [out 0 0]) [True] [] `shouldReturn` Taken+        -- with another store live, the tip waits for one that knows+        fleetReport conn keys (Just 0) 10 "a" [block 2, block 1] [block 1] `shouldReturn` True+        fleetReport conn keys (Just 0) 10 "b" [block 2, block 1] [block 1] `shouldReturn` True+        fleetAdvance conn keys 4 (Just 0) (Just (block 1)) (block 2) Nothing [] `shouldReturn` Raced+        map fst <$> poolSpends conn keys `shouldReturn` [block 7]+        -- the other store's lease runs out+        fleetReport conn keys (Just 20) 10 "a" [block 2, block 1] [block 1] `shouldReturn` True+        fleetAdvance conn keys 4 (Just 20) (Just (block 1)) (block 2) Nothing [] `shouldReturn` MovedTo (block 2)+        poolSpends conn keys `shouldReturn` []+        (Redis.runRedis conn (Redis.keys (keys.prefix <> "pool:*")) >>= either (fail . show) return)+          `shouldReturn` []+      it "reads the pool by the page, as it is at the tip that is asked for" $ \(conn, fresh) -> do+        keys <- fresh+        fleetReport conn keys (Just 0) 10 "a" [block 1] [] `shouldReturn` True+        fleetAdvance conn keys 4 (Just 0) Nothing (block 1) Nothing [] `shouldReturn` MovedTo (block 1)+        let tip = Just (block 1)+            a = address 1+        -- nine transactions, of which the fifth spends the first output of+        -- the first two+        forM_ [0 .. 8] $ \t ->+          poolOffer conn keys 4 tip (txOf t [out m 0 | t == 4, m <- [0, 1]]) [False | t == 4, _ <- [0, 1 :: Int]] []+            `shouldReturn` Taken+        poolRead conn keys 4 Nothing "page" ["0", "0"] `shouldReturn` Nothing+        -- the list of the pool: newest first, and by id downwards in a second+        Just listed <- poolRead conn keys 4 tip "page" ["0", "0"]+        pairs listed `shouldBe` [(block t, time) | (time, t) <- sortOn Down [(timeOf t, t) | t <- [0 .. 8]]]+        Just some <- poolRead conn keys 4 tip "page" ["2", "3"]+        pairs some `shouldBe` take 3 (drop 2 (pairs listed))+        -- an address: its sums, and its transactions from the top and from+        -- one of them+        let expected = P.addresses [(block t, timeOf t, entriesOf t [out m 0 | t == 4, m <- [0, 1]]) | t <- [0 .. 8]]+            mine = expected Map.! a+            totalled = P.totals mine+        Just [sums, none] <- poolRead conn keys 4 tip "sums" [a, "nobody"]+        map (read . C.unpack) (C.words sums)+          `shouldBe` [ totalled.txs,+                       totalled.paid,+                       totalled.paidCount,+                       totalled.spent,+                       totalled.spentCount,+                       totalled.both,+                       totalled.bothCount+                     ]+        none `shouldBe` ""+        totalled.bothCount `shouldSatisfy` (> 0)+        Just txs <- poolRead conn keys 4 tip "txs" [a, "", "", "100"]+        pairs txs `shouldBe` [(t, time) | (time, t) <- mine.txs]+        forM_ (zip [0 ..] mine.txs) $ \(i, (time, t)) -> do+          Just from <- poolRead conn keys 4 tip "txs" [a, C.pack (show time), t, "2"]+          pairs from `shouldBe` take 2 (drop i [(t', time') | (time', t') <- mine.txs])+        -- a transaction that is not there: from where it would be+        let (time0, t0) = (fst (last mine.txs), block 99)+        Just later <- poolRead conn keys 4 tip "txs" [a, C.pack (show time0), t0, "100"]+        pairs later `shouldBe` [(t, time) | (time, t) <- mine.txs, (Down time, t) >= (Down time0, t0)]+        -- its outputs, each with whether the pool spends it+        Just outs <- poolRead conn keys 4 tip "outs" [a, "", "", "100"]+        [(o, time, C.unpack flag) | [o, time, _, flag] <- chunks 4 outs]+          `shouldBe` [ (o, C.pack (show time), if Map.member o mine.spent then "1" else "0")+                     | (time, o, _) <- mine.paid+                     ]+        Just [flags] <- poolRead conn keys 4 tip "spent" [a, B.concat [o | (_, o, _) <- mine.paid]]+        C.unpack flags `shouldBe` [if Map.member o mine.spent then '1' else '0' | (_, o, _) <- mine.paid]+        -- bodies, spenders with the number of the input, and ids by prefix+        -- bodies come with whether they can be replaced by fee, and with+        -- what spends their outputs: the fifth says itself that it can be+        -- replaced and nothing spends from it; the second does not say so,+        -- and the second input of the fifth spends its first output+        poolRead conn keys 4 tip "bodies" [block 4 <> block 99 <> block 1]+          `shouldReturn` Just ["body", "1", "0", "", "0", "0", "body", "0", "1", out 1 0, block 4, "1"]+        poolRead conn keys 4 tip "spenders" [out 1 0 <> out 0 0 <> out 5 0]+          `shouldReturn` Just [block 4, "1", block 4, "0", "", ""]+        poolRead conn keys 4 tip "ids" [B.take 3 (block 4)] `shouldReturn` Just (map block [0 .. 8])+        poolRead conn keys 4 tip "ids" [block 4] `shouldReturn` Just [block 4]+  where+    -- carry a step out in Redis and in the model, and compare+    check conn keys (model, now) = \case+      Wait ms -> return (model, now + ms)+      Report s recent views -> do+        let name = C.pack (show s)+            allowed = mayReport now name (`elem` map block views) model+        fleetReport conn keys (Just now) lease name (map block recent) (map block views)+          `shouldReturn` allowed+        let model' = if allowed then report now lease name (map block recent) model else model+        same conn keys now model'+        return (model', now)+      Shift wrong -> do+        let seen = maybe model.tip (Just . block) wrong+            (expected, model') = case move now seen model of+              F.Moved next -> (maybe Stays MovedTo next.tip, next)+              F.Stays -> (Stays, model)+              F.Raced -> (Raced, model)+        fleetMove conn keys (Just now) seen `shouldReturn` expected+        same conn keys now model'+        return (model', now)+    nothing = PoolChange {returned = [], gone = [], confirmed = [], spent = []}+    -- carry a step of the pool out in Redis and in the model, and compare+    pooled conn keys (model, tip, n) = \case+      Give t inputs replaced -> do+        -- a transaction spends from those of lower numbers only, as none+        -- can spend from one that spends from it+        let outs = [(out a i, ok) | (a, i, ok) <- nubOrdOn (\(a, i, _) -> (a, i)) inputs, a < t]+            unspent o = lookup o outs == Just True+            (expected, model') = case P.offer maker unspent (block t) (map fst outs) (map block replaced) model of+              P.Taken next -> (Taken, next)+              P.Refused P.Known -> (Known, model)+              P.Refused (P.Conflict rivals) -> (Conflict rivals, model)+              P.Refused (P.Orphan os) -> (Orphan os, model)+        got <- poolOffer conn keys 4 tip (txOf t (map fst outs)) (map snd outs) (map block replaced)+        sorted got `shouldBe` sorted expected+        samePool conn keys model'+        return (model', tip, n)+      Take ts -> do+        let model' = P.withdraw (map block ts) model+        left <- poolWithdraw conn keys 4 (map block ts)+        Set.fromList left `shouldBe` Map.keysSet model.spends `Set.difference` Map.keysSet model'.spends+        samePool conn keys model'+        return (model', tip, n)+      Go back dead done used -> do+        -- one store, which reports a block that no step has had+        let target = block (1000 + n)+            returned' = [(block t, [out a i | (a, i) <- nubOrd ins, a < t]) | (t, ins) <- back]+            change = P.Change returned' (map block dead) (map block done) [out a i | (a, i) <- used]+            sent =+              PoolChange+                [txOf (number t) os | (t, os) <- returned']+                change.gone+                change.confirmed+                change.spent+        fleetReport conn keys (Just 0) 10 "a" [target] (maybe [] pure tip) `shouldReturn` True+        fleetAdvance conn keys 4 (Just 0) tip target (Just sent) [] `shouldReturn` MovedTo target+        let model' = P.advance maker change model+        samePool conn keys model'+        return (model', Just target, n + 1)+    maker = B.take 4+    sorted = \case+      Conflict rivals -> Conflict (sort rivals)+      other -> other+    samePool conn keys model = do+      let key k = keys.prefix <> k+          list :: Redis.Redis (Either Redis.Reply a) -> IO a+          list = fmap (either (error . show) id) . Redis.runRedis conn+      spends <- poolSpends conn keys+      Map.fromList spends `shouldBe` Map.map B.concat model.spends+      spender <- list (Redis.hgetall (key "pool:spender"))+      Map.fromList spender `shouldBe` model.spender+      kids <- list (Redis.keys (key "pool:kids:*"))+      Set.fromList kids `shouldBe` Set.map (key . ("pool:kids:" <>)) (Map.keysSet model.children)+      forM_ (Map.toList model.children) $ \(t, expected) -> do+        members <- list (Redis.smembers (key ("pool:kids:" <> t)))+        Set.fromList members `shouldBe` expected+      -- those that can be replaced by fee are those that say so and what+      -- spends from them+      canBe <- list (Redis.smembers (key "pool:rbf"))+      Set.fromList canBe `shouldBe` P.replaceable signalling maker model+      -- and what it holds for each address is what the entries of its+      -- transactions come to+      let members = [(t, timeOf (number t), entriesOf (number t) os) | (t, os) <- Map.toList model.spends]+          expected = P.addresses members+          scored :: [(ByteString, Double)] -> [(Word64, ByteString)]+          scored rows = [(round (negate s), m) | (m, s) <- rows]+      listed <- list (Redis.zrangeWithscores (key "pool:time") 0 (-1))+      [(round s, t) | (t, s) <- listed] `shouldBe` sort [(time, t) | (t, time, _) <- members]+      forM_ (map address [0 .. 3]) $ \a -> do+        txs <- list (Redis.zrangeWithscores (key ("pool:atx:" <> a)) 0 (-1))+        outs <- list (Redis.zrangeWithscores (key ("pool:aout:" <> a)) 0 (-1))+        values <- Map.fromList <$> list (Redis.hgetall (key ("pool:aval:" <> a)))+        spent <- list (Redis.hgetall (key ("pool:aspent:" <> a)))+        sums <- Map.fromList <$> list (Redis.hgetall (key ("pool:asum:" <> a)))+        let amount o = maybe 0 (read . C.unpack . C.takeWhile (/= ' ')) (Map.lookup o values)+            found =+              P.AddressPool+                { txs = scored txs,+                  paid = [(time, o, amount o) | (time, o) <- scored outs],+                  spent = Map.fromList [(o, read (C.unpack v)) | (o, v) <- spent]+                }+            sumOf f = maybe 0 (read . C.unpack) (Map.lookup f sums)+            totalled =+              P.Totals+                { txs = sumOf "txs",+                  paid = sumOf "paid",+                  paidCount = sumOf "paidCount",+                  spent = sumOf "spent",+                  spentCount = sumOf "spentCount",+                  both = sumOf "both",+                  bothCount = sumOf "bothCount"+                }+        case Map.lookup a expected of+          Nothing -> (a, found, Map.null sums) `shouldBe` (a, P.AddressPool [] [] Map.empty, True)+          Just p -> (a, found, totalled) `shouldBe` (a, p, P.totals p)+        Map.keysSet values `shouldBe` Set.fromList [o | (_, o) <- scored outs]+    same conn keys now model = do+      fleetTip conn keys `shouldReturn` (model :: F.Fleet ByteString ByteString).tip+      leases <- fleetLive conn keys+      Map.keys (Map.filter (> now) (Map.fromList leases)) `shouldBe` Map.keys (live now model)+    lease = 10++-- | The items of a list two by two, each second one a number.+pairs :: [ByteString] -> [(ByteString, Word64)]+pairs rows = [(x, read (C.unpack n)) | [x, n] <- chunks 2 rows]++-- | The items of a list so many at a time.+chunks :: Int -> [a] -> [[a]]+chunks _ [] = []+chunks n xs = take n xs : chunks n (drop n xs)++-- | A block of the tests, or a transaction: four bytes.+block :: Word32 -> ByteString+block = encode++-- | An output of a transaction of the tests.+out :: Word32 -> Word32 -> ByteString+out t i = encode t <> encode i++-- | The number of a block or of a transaction of the tests.+number :: ByteString -> Word32+number = either error id . decode++-- | One of the four addresses of the tests.+address :: Word32 -> ByteString+address n = C.pack ("address " <> show (n `mod` 4))++-- | The address that an output of the tests is paid to, and its amount.+-- Outputs of different transactions go to the same addresses, and a+-- transaction that spends an output does so at the address that it was+-- paid to, so that the pool has addresses with outputs both paid and spent.+paidTo :: ByteString -> (ByteString, Word64)+paidTo o = (address (t + i), fromIntegral (t * 10 + i + 1))+  where+    (t, i) = (number (B.take 4 o), number (B.drop 4 o))++-- | The time at which a transaction of the tests entered a mempool: one of+-- three, so that several have the same.+timeOf :: Word32 -> Word64+timeOf t = fromIntegral (t `mod` 3) + 100++-- | What a transaction of the tests that spends some outputs does at each+-- address: it pays two outputs of its own.+entriesOf :: Word32 -> [ByteString] -> [P.Entry ByteString ByteString]+entriesOf t spends =+  map P.Touches (nubOrd (map (fst . paidTo) (spends <> made)))+    <> [P.Spends a o v | o <- spends, let (a, v) = paidTo o]+    <> [P.Pays a o v | o <- made, let (a, v) = paidTo o]+  where+    made = [out t 0, out t 1]++-- | Whether a transaction of the tests says itself that it can be replaced+-- by fee: one in four does.+signalling :: ByteString -> Bool+signalling t = number t `mod` 4 == 0++-- | A transaction of the tests as the pool takes it.+txOf :: Word32 -> [ByteString] -> PoolTx+txOf t spends =+  PoolTx+    { txid = block t,+      time = timeOf t,+      inputs = spends,+      body = "body",+      signals = signalling (block t),+      entries = map entry (entriesOf t spends)+    }+  where+    entry = \case+      P.Touches a -> PoolTouches a+      P.Spends a o v -> PoolSpends a o v+      P.Pays a o v -> PoolPays a o v "script"++-- | A step of the shared mempool, with transactions numbered from 0 to 11.+data PoolStep+  = -- | a transaction is offered, spending some outputs, each found unspent+    -- at the tip or not, and replacing some transactions+    Give !Word32 ![(Word32, Word32, Bool)] ![Word32]+  | -- | transactions are withdrawn+    Take ![Word32]+  | -- | the tip moves: some transactions come back with what they spend,+    -- some are gone, some are confirmed, and some outputs are spent+    Go ![(Word32, [(Word32, Word32)])] ![Word32] ![Word32] ![(Word32, Word32)]+  deriving (Show)++instance Arbitrary PoolStep where+  arbitrary =+    frequency+      [ (8, Give <$> tx <*> few ((,,) <$> tx <*> choose (0, 1) <*> arbitrary) <*> few tx),+        (1, Take <$> few tx),+        (3, Go <$> few ((,) <$> tx <*> few output) <*> few tx <*> few tx <*> few output)+      ]+    where+      tx = choose (0, 11)+      output = (,) <$> tx <*> choose (0, 1)+      few :: Gen a -> Gen [a]+      few one = choose (0, 3) >>= (`vectorOf` one)++-- | A Redis in a container, and a way to get the keys of a fleet that no+-- test has used.+withRedis :: ((Redis.Connection, IO FleetKeys) -> IO ()) -> IO ()+withRedis go = do+  image <- testImage+  withContainer (redisContainer image) $ \c -> do+    port <- hostPort c 6379+    let url = "redis://127.0.0.1:" <> show port+    waitRedis url+    conn <- Redis.checkedConnect (either error id (Redis.parseConnectInfo url))+    counter <- newIORef (0 :: Int)+    go (conn, (\n -> fleetKeys (C.pack ("t" <> show n <> ":")) 4) <$> atomicModifyIORef' counter (\n -> (n + 1, n)))
+ test/Haskoin/Store/Fleet/XPubSpec.hs view
@@ -0,0 +1,77 @@+module Haskoin.Store.Fleet.XPubSpec (spec) where++import Data.Functor.Identity (Identity (..))+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.Serialize (encode)+import Data.Word (Word32)+import Haskoin (Address (..), addressHash)+import Haskoin.Store.Common (scanXPubBals)+import Haskoin.Store.Data (Balance (..), BlockRef (..), XPubBal (..), zeroBalance)+import Haskoin.Store.Fleet.XPub+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++spec :: Spec+spec = describe "cache of extended public keys of a fleet" $ do+  describe "score" $ do+    prop "is undone by position, in the chain" $ \(h, p) ->+      let ref = BlockRef {height = h `mod` 0x08000000, position = p `mod` 0x04000000}+       in position (score ref) === ref+    prop "is undone by position, in the mempool" $ \t ->+      let ref = MemRef {timestamp = fromIntegral (t :: Word32) + 1}+       in position (score ref) === ref+    prop "puts the newest first" $ \(a, b) ->+      let ref (h, p) = BlockRef {height = h `mod` 0x08000000, position = p `mod` 0x04000000}+       in compare (score (ref a)) (score (ref b)) === compare (ref b) (ref a)+    it "puts the mempool before the chain, newest first" $ do+      score (MemRef 20) `shouldSatisfy` (< score (MemRef 10))+      score (MemRef 10) `shouldSatisfy` (< score (BlockRef 500 0))+  describe "window" $ do+    -- which addresses of the two chains of a key are used, and the gaps+    let key = do+          igap <- choose (1, 8)+          gap <- choose (1, 8)+          ext <- HashSet.fromList <$> listOf (choose (0, 30 :: Word32))+          chg <- HashSet.fromList <$> listOf (choose (0, 30 :: Word32))+          return (igap, gap, ext, chg)+        address m n = PubKeyAddress (addressHash (encode (m :: Word32, n :: Word32)))+        balance used m n =+          let zero = zeroBalance (address m n)+           in if HashSet.member n used then zero {txs = 1, received = 5} else zero+        scan (igap, gap, ext, chg) =+          let chain m = [(n, address m n) | n <- [0 ..]]+              known =+                HashMap.fromList $+                  [(address 0 n, balance ext 0 n) | n <- [0 .. 80]]+                    <> [(address 1 n, balance chg 1 n) | n <- [0 .. 80]]+              bal a = HashMap.lookupDefault (zeroBalance a) a known+           in runIdentity (scanXPubBals (return . map bal) igap gap (chain 0) (chain 1))+        rows found = [(m, n, b.balance) | b@XPubBal {path = [m, n]} <- found]+    it "gives the scan when the entry has the addresses that it reads" $+      property . forAll key $ \k@(igap, gap, _, _) ->+        let found = scan k in window igap gap (rows found) === Just found+    it "gives nothing when the entry lacks the last address of a chain" $+      property . forAll key $ \k@(igap, gap, _, _) ->+        let found = scan k+            final m = maximum [n | (m', n, _) <- rows found, m' == m]+            lacking m = [r | r@(m', n, _) <- rows found, m' /= m || n /= final m]+         in conjoin [window igap gap (lacking m) === Nothing | m <- [0, 1], any (\(m', _, _) -> m' == m) (rows found)]+    it "gives nothing when the entry has an address more" $+      property . forAll key $ \k@(igap, gap, ext, chg) ->+        let found = scan k+            next m = length [() | (m', _, _) <- rows found, m' == m]+            more m used = rows found <> [(m, fromIntegral (next m), balance used m (fromIntegral (next m)))]+            -- one more is another key, unless the scan read no change+            -- addresses at all and the one more is the first of them+            changeless = next 1 == 0+         in conjoin+              [ window igap gap (more 0 ext) === Nothing,+                changeless .||. window igap gap (more 1 chg) === Nothing+              ]+    it "gives nothing when a chain has a hole" $+      property . forAll key $ \k@(igap, gap, _, _) ->+        let found = scan k+            holed = [r | r@(m, n, _) <- rows found, (m, n) /= (0, 0)]+         in length found > 1 ==> window igap gap holed === Nothing
+ test/Haskoin/Store/FleetSpec.hs view
@@ -0,0 +1,199 @@+module Haskoin.Store.FleetSpec (spec) where++import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Set (Set)+import Data.Set qualified as Set+import Haskoin.Store.Fleet+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck++-- | A store of the model: its main chain, newest block first, and the blocks+-- that it has views of.+data Node = Node+  { chain :: ![Int],+    views :: !(Set Int)+  }+  deriving (Show)++-- | A fleet of three stores, the chain that the network has, newest block+-- first, the state in Redis, and the time.+data World = World+  { network :: ![Int],+    fresh :: !Int,+    nodes :: !(Map Int Node),+    redis :: !(Fleet Int Int),+    now :: !Time+  }+  deriving (Show)++-- | Something that happens.+data Op+  = -- | the network finds a block+    Grow+  | -- | the network replaces its last blocks, as many as given, with as many+    -- new ones and one more+    Reorg !Int+  | -- | a store catches up with the network by so many steps, each the+    -- reversal or the import of one block+    Sync !Int !Int+  | -- | a store reports, moves the tip if it can, and lets go of the views+    -- that it need not keep+    Beat !Int+  | -- | time passes+    Tick !Time+  deriving (Show)++instance Arbitrary Op where+  arbitrary =+    frequency+      [ (3, pure Grow),+        (1, Reorg <$> choose (1, 3)),+        (8, Sync <$> choose (0, 2) <*> choose (1, 4)),+        (8, Beat <$> choose (0, 2)),+        (3, Tick <$> choose (1, 6))+      ]++-- | How a store of the model behaves: whether it asks before it reports,+-- and whether it keeps the views that the rules tell it to.+data Rules = Rules+  { asks :: !Bool,+    keeps :: !Bool+  }++byTheRules :: Rules+byTheRules = Rules {asks = True, keeps = True}++-- | Leases last ten ticks, and a store reports its six newest blocks.+lease :: Time+lease = 10++window :: Int+window = 6++start :: World+start =+  World+    { network = [0],+      fresh = 1,+      nodes = Map.fromList [(s, Node {chain = [0], views = Set.singleton 0}) | s <- [0 .. 2]],+      redis = empty,+      now = 0+    }++step :: Rules -> World -> Op -> World+step rules w = \case+  Grow -> w {network = w.fresh : w.network, fresh = w.fresh + 1}+  Reorg n ->+    let kept = drop (min n (length w.network - 1)) w.network+        new = [w.fresh + n, w.fresh + n - 1 .. w.fresh]+     in w {network = new <> kept, fresh = w.fresh + n + 1}+  Sync s n -> w {nodes = Map.adjust (\node -> iterate catchUp node !! n) s w.nodes}+  Tick dt -> w {now = w.now + dt}+  Beat s ->+    let node = w.nodes Map.! s+        has b = Set.member b node.views+        -- only what it still has a view of: a view that was let go cannot be+        -- had again+        blocks = filter has (take window node.chain)+        allowed = not rules.asks || mayReport w.now s has w.redis+        reported = report w.now lease s blocks w.redis+        moved = case move w.now reported.tip reported of+          Moved fleet -> fleet+          _ -> reported+        kept+          | rules.keeps = Set.fromList (keep blocks moved.tip) `Set.intersection` node.views+          | otherwise = Set.fromList (take 1 node.chain)+     in if allowed+          then w {redis = moved, nodes = Map.insert s node {views = kept} w.nodes}+          else w+  where+    -- one step towards the chain of the network: revert a block that it+    -- does not have, or import the next one that it has+    catchUp node+      | b : rest <- node.chain, b `notElem` w.network = node {chain = rest}+      | next : _ <- reverse (takeWhile (`notElem` node.chain) w.network) =+          node {chain = next : node.chain, views = Set.insert next node.views}+      | otherwise = node++-- | The stores whose lease has not run out and that have no view of the+-- tip: those that would have to refuse a request although they are counted+-- in.+stranded :: World -> [Int]+stranded w =+  [ s+  | Just t <- [w.redis.tip],+    s <- Map.keys (live w.now w.redis),+    not (Set.member t (w.nodes Map.! s).views)+  ]++-- | Every state that a list of operations goes through.+states :: Rules -> [Op] -> [World]+states rules = scanl (step rules) start++spec :: Spec+spec = describe "fleet tip" $ do+  it "is the newest block that every live store reported" $ do+    let fleet = foldl' (\f (s, bs) -> report 0 10 s bs f) (empty :: Fleet Int Int) reports'+        reports' = [(0, [5, 4, 3, 2]), (1, [4, 3, 2, 1]), (2, [7, 6, 3, 2])]+    agreed 0 fleet `shouldBe` Just 3+    -- a store whose lease has run out no longer counts+    agreed 12 (report 12 10 0 [5, 4, 3] (report 12 10 1 [4, 3] fleet)) `shouldBe` Just 4+    agreed 20 fleet `shouldBe` Nothing++  it "moves once when two stores try at the same time" $ do+    let fleet = report 0 10 0 [2, 1] (report 0 10 1 [2, 1] (empty :: Fleet Int Int))+    case move 0 Nothing fleet of+      Moved next -> do+        next.tip `shouldBe` Just 2+        -- the second store saw the same tip as the first, and is too late+        move 0 Nothing next `shouldBe` Raced+        move 0 (Just 2) next `shouldBe` Stays+      other -> expectationFailure (show other)++  it "keeps a store that is behind from pulling the tip back" $ do+    let ahead = report 0 10 0 [9, 8, 7, 6] (empty :: Fleet Int Int)+        fleet = case move 0 Nothing ahead of+          Moved f -> f+          _ -> ahead+    fleet.tip `shouldBe` Just 9+    mayReport 1 1 (`elem` [7, 6, 5 :: Int]) fleet `shouldBe` False+    mayReport 1 1 (`elem` [9, 8, 7 :: Int]) fleet `shouldBe` True+    -- when the store that was ahead is gone, the other may start again+    mayReport 11 1 (`elem` [7, 6, 5 :: Int]) fleet `shouldBe` True++  it "answers from the tip, from its own block before there is one, or not at all" $ do+    let fleet = (empty :: Fleet Int Int) {tip = Just 4}+    answer (const True) (empty :: Fleet Int Int) `shouldBe` Alone+    answer (== 4) fleet `shouldBe` At 4+    answer (== 5) fleet `shouldBe` Behind++  modifyMaxSuccess (const 2000) $ do+    it "never leaves a live store without a view of the tip" $+      property $ \ops ->+        let trouble = [(w.redis.tip, stranded w) | w <- states byTheRules ops, not (null (stranded w))]+         in counterexample (show (take 1 trouble)) (null trouble)++    it "moves along and across branches, never to a block that no store has" $+      property $ \ops ->+        let tips = [t | w <- states byTheRules ops, Just t <- [w.redis.tip]]+            made w = w.fresh+         in conjoin+              [ cover 5 (length (Set.fromList tips) > 3) "the tip was at more than three blocks" True,+                property (all (< made (last (states byTheRules ops))) tips)+              ]++    it "ends at the best block once every store has caught up and reported" $+      property $ \ops ->+        let settled = foldl' (step byTheRules) (last (states byTheRules ops)) settle+            settle = [Tick 20] <> [Sync s 100 | s <- [0 .. 2]] <> [Beat s | s <- [0 .. 2]] <> [Beat 0]+         in (maybe [] pure settled.redis.tip, stranded settled) === (take 1 settled.network, [])++    it "needs the store to ask before it reports" $+      expectFailure . property $ \ops ->+        all (null . stranded) (states byTheRules {asks = False} ops)++    it "needs the store to keep the views of what it reported" $+      expectFailure . property $ \ops ->+        all (null . stranded) (states byTheRules {keeps = False} ops)
+ test/Haskoin/Store/Histories.hs view
@@ -0,0 +1,783 @@+-- | Random histories of a store, for tests that compare two ways of keeping+-- its database: the layout in use today, where the chain and the mempool+-- share their indices, and the layered layout of @docs/layered-database.md@.+--+-- A history is a sequence of operations: transactions for the mempool,+-- blocks, deletions and reversals, some of them invalid. Each is chosen from+-- a number and from what a database of today's layout holds at that point,+-- so that the same history can be given to both sets of rules.+--+-- Payments go to a handful of fixed scripts and to a few addresses of an+-- extended public key, 'wallet', so that the queries about extended public+-- keys have something to find.+module Haskoin.Store.Histories+  ( -- * Databases and rules+    open20,+    openLayered,+    withShadow,+    Rules (..),+    mixedRules,+    layeredRules,+    run,++    -- * Histories+    History (..),+    Op (..),+    Outcome (..),+    start,+    plan,+    apply,+    record,+    step,+    blockOn,+    coinOf,+    payments,+    anyNumber,+    onlyPayments,+    p2pkh,+    hashLock,+    addressesOf,+    wallet,++    -- * Answers+    mixed,+    layered,+    pooled,+    Answers (..),+    ask,++    -- * Pages+    Question (..),+    Reply (..),+    questions,+    answer,+    differences,+    startOf,+    startsKnown,+    size,++    -- * Column families+    Chain (..),+    chainOf,+    dump,+  )+where++import Control.Monad (void)+import Data.Bifunctor (first)+import Data.Bits (shiftR)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Default (def)+import Data.Either (isLeft, rights)+import Data.Hashable (hashWithSalt)+import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IntMap+import Data.List (sort, sortOn)+import Data.Maybe (catMaybes, fromMaybe, mapMaybe)+import Data.Serialize (decode, encode, getWord64be, runGet)+import Data.Word (Word16, Word32, Word64, Word8)+import Database.RocksDB (ColumnFamily, iterEntry, iterFirst, iterNext, withIterCF)+import Haskoin+  ( Address,+    Block (..),+    BlockHash,+    BlockHeader (..),+    BlockHeight,+    BlockNode (..),+    Ctx,+    Network,+    OutPoint (..),+    Tx (..),+    TxHash (..),+    TxIn (..),+    TxOut (..),+    addressToScriptBS,+    deriveXPubKey,+    headerHash,+    makeXPrvKey,+    nullOutPoint,+    pubSubKey,+    scriptToAddressBS,+    sha256,+    txHash,+  )+import Haskoin.Node (LogLevel (..), Logger (..))+import Haskoin.Store.Common+  ( Limits (..),+    Start (..),+    StoreRead (..),+    deriveAddresses,+    deriveFunction,+    sortTxs,+  )+import Haskoin.Store.Data+  ( Balance (..),+    BlockData (..),+    BlockRef (..),+    DeriveType (..),+    Prev (..),+    Spender (..),+    TxData (..),+    TxRef (..),+    UnixTime,+    Unspent (..),+    XPubBal (..),+    XPubSpec (..),+    XPubUnspent (..),+    confirmed,+    zeroBalance,+  )+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Query.Pooled qualified as P+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    addrOutCF,+    addrTxCF,+    balanceCF,+    spenderCF,+    txCF,+    unspentCF,+    withDatabaseReader,+    withLayeredReader,+  )+import Haskoin.Store.Database.Types (BalVal (..), SpenderKey (..), SpenderVal (..))+import Haskoin.Store.Database.Writer (MempoolChanges (..), Writer, runWriterMempool)+import Haskoin.Store.Logic (ImportException)+import Haskoin.Store.Logic qualified as Mixed+import Haskoin.Store.Logic.Layered qualified as Layered+import Haskoin.Store.TestBlocks (nextBlock, script)+import Test.QuickCheck (Gen, choose, elements, frequency, vectorOf)+import UnliftIO (try, withSystemTempDirectory)++-------------------------------------------------------------------------------+-- Databases and rules+-------------------------------------------------------------------------------++-- | Open a database as the store does today, and as the layered layout+-- needs it.+open20, openLayered :: Network -> Ctx -> FilePath -> (DatabaseReader -> IO a) -> IO a+open20 net ctx dir = withDatabaseReader net ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing+openLayered net ctx dir = withLayeredReader net ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing++-- | Run an action with a new database of the layout of version 20 that has+-- the start of a history ('start'), in a directory of its own.+--+-- 'plan' chooses the next operation of a history by reading a database of+-- that layout: what is unspent, what the mempool has. Given a database of+-- the layered layout it finds almost none of the mempool, and its blocks+-- confirm next to nothing. So a test of the layered layout keeps one of+-- these beside its own database, plans from it, and applies each+-- operation to both, as 'mixedRules' and 'layeredRules' take the same+-- operations and give the same outcomes.+withShadow :: Network -> Ctx -> (DatabaseReader -> IO a) -> IO a+withShadow net ctx action =+  withSystemTempDirectory "haskoin-store-shadow-" $ \dir ->+    open20 net ctx dir $ \shadow -> start mixedRules shadow >> action shadow++-- | The rules that import blocks and transactions, for one layout.+data Rules = Rules+  { initBest :: Writer -> IO (),+    importBlock :: Writer -> Block -> BlockNode -> IO (BlockData, [TxData]),+    revertBlock :: Writer -> BlockHash -> IO (),+    newMempoolTx :: Writer -> Tx -> UnixTime -> IO Bool,+    deleteUnconfirmedTx :: Writer -> Bool -> TxHash -> IO ()+  }++-- | The rules of the layout in use today.+mixedRules :: Rules+mixedRules =+  Rules+    { initBest = Mixed.initBest,+      importBlock = Mixed.importBlock,+      revertBlock = Mixed.revertBlock,+      newMempoolTx = Mixed.newMempoolTx,+      deleteUnconfirmedTx = Mixed.deleteUnconfirmedTx+    }++-- | The rules of the layered layout.+layeredRules :: Rules+layeredRules =+  Rules+    { initBest = Layered.initBest,+      importBlock = Layered.importBlock,+      revertBlock = Layered.revertBlock,+      newMempoolTx = Layered.newMempoolTx,+      deleteUnconfirmedTx = Layered.deleteUnconfirmedTx+    }++-- | Run a batch of writes, and give what it returned with how it changed+-- the mempool, or the reason why it wrote nothing.+run :: DatabaseReader -> (Writer -> IO a) -> IO (Either ImportException (a, MempoolChanges))+run db = try . runWriterMempool db.net db.ctx db quiet+  where+    quiet = Logger {level = LevelError, source = "", push = \_ -> return ()}++-------------------------------------------------------------------------------+-- Histories+-------------------------------------------------------------------------------++-- | What happened to a database: its main chain, newest block first, every+-- transaction ever made for it, taken or not, coinbases included, the+-- outputs made that have no address, which no index of addresses lists, the+-- time to give the next mempool transaction, and the scripts that payments+-- go to.+data History = History+  { chain :: ![(Block, BlockNode)],+    made :: ![Tx],+    loose :: ![OutPoint],+    clock :: !UnixTime,+    scripts :: ![ByteString]+  }++-- | Something that happens to a store.+data Op+  = -- | a transaction arrives for the mempool, at a time+    Pay !Tx !UnixTime+  | -- | a block arrives, on the best block+    Mine !Block !BlockNode+  | -- | a mempool transaction is deleted, with its descendants+    Delete !TxHash+  | -- | the best block is reverted+    Revert !BlockHash+  deriving (Show)++-- | What an operation did: what the rules returned or why they refused, and+-- the transactions that entered and left the mempool.+data Outcome = Outcome+  { result :: !(Either ImportException String),+    added :: ![TxHash],+    removed :: ![TxHash]+  }+  deriving (Eq, Show)++-- | Give an operation to a set of rules.+apply :: Rules -> DatabaseReader -> Op -> IO Outcome+apply rules db = \case+  Pay tx time -> outcome <$> run db (\w -> rules.newMempoolTx w tx time)+  -- the data of the transactions is left out: it comes with the spenders+  -- that each layout keeps in a body+  Mine block node -> outcome . fmap (first fst) <$> run db (\w -> rules.importBlock w block node)+  Delete h -> outcome <$> run db (\w -> rules.deleteUnconfirmedTx w False h)+  Revert h -> outcome <$> run db (\w -> rules.revertBlock w h)+  where+    outcome :: (Show a) => Either ImportException (a, MempoolChanges) -> Outcome+    outcome (Left e) = Outcome {result = Left e, added = [], removed = []}+    outcome (Right (x, c)) = Outcome {result = Right (show x), added = sort c.added, removed = sort c.removed}++-- | Note what an operation did in a history. Transactions are noted whether+-- they were taken or not, so that questions are asked about all of them.+record :: DatabaseReader -> History -> Op -> Outcome -> History+record db hist op out = case op of+  Pay tx _ -> noted [tx]+  Mine block node+    | isLeft out.result -> noted block.txs+    | otherwise -> (noted block.txs) {chain = (block, node) : hist.chain}+  Delete _ -> hist+  Revert _+    | isLeft out.result -> hist+    | otherwise -> hist {chain = drop 1 hist.chain}+  where+    known = map txHash hist.made+    noted txs =+      let new = [tx | tx <- txs, txHash tx `notElem` known]+          unlisted =+            [ OutPoint (txHash tx) i+            | tx <- new,+              (i, o) <- zip [0 ..] tx.outputs,+              isLeft (scriptToAddressBS db.net db.ctx o.script)+            ]+       in hist {made = new <> hist.made, loose = unlisted <> hist.loose}++-- | A database with the genesis block and one block with a coin, under a+-- set of rules, and its history so far.+start :: Rules -> DatabaseReader -> IO History+start rules db = do+  void (run db rules.initBest) :: IO ()+  let hist = History {chain = [], made = [], loose = [], clock = 1000, scripts = places db.ctx}+  block <- blockOn db []+  record db hist block <$> apply rules db block++-- | A block with these transactions on the best block of a database.+blockOn :: DatabaseReader -> [Tx] -> IO Op+blockOn db txs = do+  best <- Q.getBestBlock db >>= maybe (fail "no best block") return+  parent <- Q.getBlock db best >>= maybe (fail "no data for the best block") return+  return (uncurry Mine (nextBlock db.net parent.height parent.header txs))++-- | The output of the coinbase of the best block.+coinOf :: History -> OutPoint+coinOf hist = case hist.chain of+  (Block {txs = coinbase : _}, _) : _ -> OutPoint (txHash coinbase) 0+  _ -> error "no block with a coinbase"++-- | The scripts that the payments of a history go to: one that has no+-- address on Bitcoin, that of every coinbase, four more of the same kind,+-- and those of five addresses of the 'wallet'.+places :: Ctx -> [ByteString]+places ctx = hashLock : script : map p2pkh [1 .. 4] <> mapMaybe (addressToScriptBS ctx) paid+  where+    spec = wallet ctx+    branch m = map snd (deriveAddresses (deriveFunction ctx spec.deriv) (pubSubKey ctx spec.key m) 0)+    -- with addresses that are never paid between them, so that whether the+    -- later ones are within the gap depends on which earlier ones are used+    paid = map (branch 0 !!) [0, 2, 5] <> map (branch 1 !!) [0, 2]++-- | An extended public key. Five of its addresses are paid in histories:+-- three that it gives out to be paid, and two for its change.+wallet :: Ctx -> XPubSpec+wallet ctx = XPubSpec {key = deriveXPubKey ctx (makeXPrvKey seed), deriv = DeriveNormal}+  where+    seed = "the wallet of the histories of the tests of haskoin-store" :: ByteString++p2pkh :: Word8 -> ByteString+p2pkh n = "\x76\xa9\x14" <> B.replicate 20 n <> "\x88\xac"++-- | A script that has no address on Bitcoin.+hashLock :: ByteString+hashLock = "\xaa\x20" <> B.replicate 32 0xbb <> "\x87"++-- | The addresses of the scripts of a history that have one on a network.+addressesOf :: Network -> Ctx -> History -> [Address]+addressesOf net ctx hist = rights (map (scriptToAddressBS net ctx) hist.scripts)++-- | Eight payments, each from the change of the one before, starting from+-- a coin: a chain in the mempool.+payments :: OutPoint -> [Tx]+payments coin = take 8 (go coin 5000000000)+  where+    go from value =+      let left = value - 10000+          tx = Tx 2 [TxIn from "" maxBound] [TxOut left script, TxOut 9000 (p2pkh 3)] [] 0+       in tx : go (OutPoint (txHash tx) 0) left++-- | A number for 'plan', any of them as likely as any other. QuickCheck's+-- own generator of numbers starts with small ones, which would make the+-- first histories of every run nothing but the same payment.+anyNumber :: Gen Word16+anyNumber = choose (minBound, maxBound)++-- | Turn numbers into numbers that 'plan' reads as payments.+onlyPayments :: [Word16] -> [Word16]+onlyPayments ps = [p - p `mod` 16 + p `mod` 6 | p <- ps]++-- | Choose and carry out one operation under the rules of today's layout,+-- which is the layout that 'plan' reads.+step :: DatabaseReader -> History -> Word16 -> IO History+step db hist seed =+  plan db hist seed >>= \case+    (hist', Nothing) -> return hist'+    (hist', Just op) -> record db hist' op <$> apply mixedRules db op++-- | Choose the next operation of a history from a number, given a database+-- of today's layout that has gone through the history so far. Out of+-- sixteen: six are payments between the scripts of the history, from outputs+-- that are unspent, in a block or in the mempool, a few of them with ten+-- outputs and the others with up to three; one is a transaction that+-- must be refused; one conflicts with a mempool transaction; four are+-- blocks, with part of the mempool, with a transaction that the mempool has+-- not seen, or with one that conflicts with the mempool; two are deletions;+-- one is the reversal of the best block; and one sends an earlier+-- transaction again. The clock moves on with some of them, so that mempool+-- transactions share a time now and then.+plan :: DatabaseReader -> History -> Word16 -> IO (History, Maybe Op)+plan db hist0 seed = (hist,) <$> chosen+  where+    hist = hist0 {clock = hist0.clock + fromIntegral (seed `mod` 2)}+    kind = seed `mod` 16+    s = fromIntegral seed `div` 16 :: Int+    chosen+      | kind <= 5 = pay+      | kind == 6 = refused+      | kind == 7 = rival (\tx -> return (Just (Pay tx hist.clock)))+      | kind <= 9 = block . shuffled =<< some+      | kind == 10 = rival (\tx -> Just <$> blockOn db [tx]) >>= maybe (block []) (return . Just)+      | kind == 11 = unseen+      | kind == 12 = delete+      | kind == 13 = deleteOther+      | kind == 14 = revert+      | otherwise = again+    rotate k xs = let k' = k `mod` max 1 (length xs) in drop k' xs <> take k' xs+    target j = hist.scripts !! ((s `div` 11 + j) `mod` length hist.scripts)+    spending us values =+      let outputs = [TxOut v (target j) | (j, v) <- zip [0 ..] values]+       in Tx 2 [TxIn u.outpoint "" signal | u <- us] outputs [] 0+    -- some signal that they can be replaced+    signal = if even (s `div` 13) then maxBound else 0+    block txs = Just <$> blockOn db txs+    shuffled = sortOn (hashWithSalt s . encode . txHash)+    pool = map snd <$> Q.getMempool db+    -- outputs that nothing spends, in a block or in the mempool+    unspent = do+      listed <- concat <$> mapM (\a -> Q.getAddressUnspents db a def) (addressesOf db.net db.ctx hist)+      (listed <>) . catMaybes <$> mapM (Q.getUnspent db) hist.loose+    -- a payment from some of them, in equal shares: now and then in ten of+    -- them, for a transaction with many outputs to spend+    payment us =+      let total = sum (map (.value) us)+          count = if s `mod` 13 == 5 && total >= 50000 then 10 else 1 + (s `div` 7) `mod` 3+          share = (total - 500) `div` fromIntegral count+       in if null us || total < 500 + 600 * fromIntegral count+            then Nothing+            else Just (spending us (replicate count share))+    pay = do+      us <- take (1 + s `mod` 3) . rotate (s `div` 3) <$> unspent+      return ((`Pay` hist.clock) <$> payment us)+    -- part of the mempool, in an order in which it can be mined+    some = do+      tds <- catMaybes <$> (pool >>= Q.getManyTxData db)+      let ordered = map snd (sortTxs (map (.tx) tds))+      return (take (s `mod` (length ordered + 1)) ordered)+    -- a block with part of the mempool and a payment that the mempool has+    -- not seen, from outputs of blocks+    unseen = do+      us <- take (1 + s `mod` 2) . rotate (s `div` 3) . filter (confirmed . (.block)) <$> unspent+      txs <- some+      block (shuffled (txs <> maybe [] pure (payment us)))+    -- a transaction that spends what a mempool transaction spends, when+    -- that is an output of a block+    rival :: (Tx -> IO (Maybe Op)) -> IO (Maybe Op)+    rival make =+      rotate s <$> pool >>= \case+        [] -> return Nothing+        h : _ ->+          Q.getTxData db h >>= \case+            Just td+              | i : _ <- td.tx.inputs,+                Just p <- IntMap.lookup 0 td.prevs,+                p.value > 2000 ->+                  Q.getTxData db i.outpoint.hash >>= \case+                    Just parent+                      | confirmed parent.block ->+                          let value = p.value - 700 - fromIntegral (s `mod` 50)+                           in make (Tx 2 [TxIn i.outpoint "" maxBound] [TxOut value (target 0)] [] 0)+                    _ -> return Nothing+            _ -> return Nothing+    -- a transaction or a block that the rules must refuse+    refused = do+      us <- rotate (s `div` 8) <$> unspent+      let spent = [i.outpoint | (b, _) <- hist.chain, mined <- drop 1 b.txs, i <- take 1 mined.inputs]+          unknown = Tx 2 [TxIn (OutPoint (TxHash (sha256 (encode seed))) 0) "" maxBound] [TxOut 1000 script] [] 0+          again' p = Tx 2 [TxIn p "" maxBound] [TxOut 1000 script] [] 0+          send tx = return (Just (Pay tx hist.clock))+      case (s `mod` 8, us, rotate s spent) of+        -- an output that a confirmed transaction has spent+        (1, _, p : _) -> send (again' p)+        -- the same output twice+        (2, u : _, _) -> send (Tx 2 (replicate 2 (TxIn u.outpoint "" maxBound)) [TxOut 1000 script] [] 0)+        -- more than it has+        (3, u : _, _) -> send (Tx 2 [TxIn u.outpoint "" maxBound] [TxOut (u.value + 1) script] [] 0)+        -- a coinbase+        (4, _, _) -> send (Tx 1 [TxIn nullOutPoint (encode seed) maxBound] [TxOut 1000 script] [] 0)+        -- a block that spends an output that does not exist+        (5, _, _) -> block [unknown]+        -- a block that spends an output that a confirmed transaction has+        -- spent+        (6, _, p : _) -> block [again' p]+        -- a block that does not build on the best block, but beside it+        (7, _, _) | (_, best) : _ <- hist.chain -> do+          parent <- Q.getBlock db best.header.prev >>= maybe (fail "no data for the parent") return+          return . Just $ uncurry Mine (nextBlock db.net parent.height parent.header [])+        -- an output that does not exist+        _ -> send unknown+    delete =+      fmap Delete . take 1 . rotate s <$> pool >>= \case+        [] -> return Nothing+        d : _ -> return (Just d)+    -- the deletion of a confirmed transaction, or of one that is not there+    deleteOther =+      let mined = [txHash tx | (b, _) <- hist.chain, tx <- b.txs]+       in return . Just . Delete $ case rotate s mined of+            h : _ | even s -> h+            _ -> TxHash (sha256 (encode seed))+    revert = return $ case hist.chain of+      (b, _) : _ : _ -> Just (Revert (headerHash b.header))+      _ -> Nothing+    -- an earlier transaction, which may be confirmed, in the mempool,+    -- deleted, or missing what it spends+    again = return $ case rotate s hist.made of+      tx : _ -> Just (Pay tx hist.clock)+      [] -> Nothing++-------------------------------------------------------------------------------+-- Answers+-------------------------------------------------------------------------------++-- | Run an action with the queries of a database: those of today's layout,+-- where one set of indices has the chain and the mempool mixed, or those of+-- the layered layout, on a view of the database as it is now.+mixed, layered, pooled :: DatabaseReader -> (StoreRead -> IO a) -> IO a+mixed db action = action (Q.databaseRead db)+layered db action = L.withView db (action . L.layeredRead)+-- the queries of a fleet, with the store's own mempool area as the pool+pooled db action = L.withView db $ \v ->+  let r = L.viewReader v in action (P.pooledRead r (P.localPool r))++-- | Everything that the API could say about a history, in whole lists.+data Answers = Answers+  { balances :: ![(Address, Balance)],+    addressTxs :: ![(Address, [TxRef])],+    addressUnspents :: ![(Address, [Unspent])],+    txs :: ![(TxHash, Maybe TxData)],+    numbered :: ![(TxHash, [TxData])],+    spenders :: ![(OutPoint, Maybe Spender)],+    unspents :: ![(OutPoint, Maybe Unspent)],+    mempool :: ![(UnixTime, TxHash)]+  }+  deriving (Eq, Show)++-- | Ask everything about the addresses of a history and about every+-- transaction made, by its hash and by its short number, with each of its+-- outputs and one that it does not have. An address that was never used and+-- one whose balance is zero are the same to the API, and are the same here.+ask :: History -> StoreRead -> IO Answers+ask hist queries = do+  balances <- mapM (\a -> (a,) . fromMaybe (zeroBalance a) <$> queries.getBalance a) as+  addressTxs <- mapM (\a -> (a,) <$> queries.getAddressTxs a def) as+  addressUnspents <- mapM (\a -> (a,) <$> queries.getAddressUnspents a def) as+  txs <- mapM (\h -> (h,) <$> queries.getTxData h) hashes+  numbered <- mapM (\h -> (h,) <$> queries.getNumTxData (shortNumber h)) hashes+  spenders <- mapM (\p -> (p,) <$> queries.getSpender p) outpoints+  unspents <- mapM (\p -> (p,) <$> queries.getUnspent p) outpoints+  mempool <- queries.getMempool+  return Answers {balances, addressTxs, addressUnspents, txs, numbered, spenders, unspents, mempool}+  where+    as = addressesOf queries.net queries.ctx hist+    hashes = map txHash hist.made+    outpoints = [OutPoint (txHash tx) i | tx <- hist.made, i <- [0 .. fromIntegral (length tx.outputs)]]++-------------------------------------------------------------------------------+-- Pages+-------------------------------------------------------------------------------++-- | A question whose answer is a page of a list, or is read in a way that+-- the questions of 'ask' do not use: many at a time, by number, or through+-- an extended public key.+data Question+  = -- | a page of the transactions of an address+    AddressTxs !Address !Limits+  | -- | a page of the transactions of some addresses+    AddressesTxs ![Address] !Limits+  | -- | a page of the unspent outputs of an address+    AddressUnspents !Address !Limits+  | -- | a page of the unspent outputs of some addresses+    AddressesUnspents ![Address] !Limits+  | -- | the balances of some addresses+    Balances ![Address]+  | -- | the data of some transactions+    Bodies ![TxHash]+  | -- | the transactions with a short number of the Blockchain.info API+    Numbered !Word64+  | -- | a page of the mempool+    Pool !Limits+  | -- | the balances of the addresses of the 'wallet'+    WalletBalances+  | -- | a page of the transactions of the 'wallet'+    WalletTxs !Limits+  | -- | a page of the unspent outputs of the 'wallet'+    WalletUnspents !Limits+  | -- | the number of transactions of the 'wallet'+    WalletCount+  | -- | a page of the transactions of some addresses, read as those of an+    -- extended public key are+    SetTxs ![Address] !Limits+  | -- | a page of the unspent outputs of some addresses, read as those of+    -- an extended public key are+    SetUnspents ![Address] !Limits+  | -- | the number of distinct transactions of some addresses+    SetCount ![Address]+  deriving (Eq, Show)++-- | The answer to a 'Question'.+data Reply+  = Refs ![TxRef]+  | Outputs ![Unspent]+  | Amounts ![Balance]+  | Data ![Maybe TxData]+  | Matches ![TxData]+  | Listed ![(UnixTime, TxHash)]+  | Paths ![XPubBal]+  | PathOutputs ![XPubUnspent]+  | Number !Word32+  deriving (Eq, Show)++-- | Ask a question.+answer :: StoreRead -> Question -> IO Reply+answer queries = \case+  AddressTxs a limits -> Refs <$> queries.getAddressTxs a limits+  AddressesTxs as limits -> Refs <$> queries.getAddressesTxs as limits+  AddressUnspents a limits -> Outputs <$> queries.getAddressUnspents a limits+  AddressesUnspents as limits -> Outputs <$> queries.getAddressesUnspents as limits+  Balances as -> Amounts <$> queries.getBalances as+  Bodies hs -> Data <$> queries.getManyTxData hs+  Numbered i -> Matches <$> queries.getNumTxData i+  Pool limits -> Listed <$> queries.getMempoolPage limits+  WalletBalances -> Paths <$> queries.xPubBals spec+  WalletTxs limits -> Refs <$> (queries.xPubBals spec >>= \bals -> queries.xPubTxs spec bals limits)+  WalletUnspents limits -> PathOutputs <$> (queries.xPubBals spec >>= \bals -> queries.xPubUnspents spec bals limits)+  WalletCount -> Number <$> (queries.xPubBals spec >>= queries.xPubTxCount spec)+  SetTxs as limits -> Refs <$> (set as >>= \bals -> queries.xPubTxs spec bals limits)+  SetUnspents as limits -> PathOutputs <$> (set as >>= \bals -> queries.xPubUnspents spec bals limits)+  SetCount as -> Number <$> (set as >>= queries.xPubTxCount spec)+  where+    spec = wallet queries.ctx+    -- the queries about an extended public key take the balances of its+    -- addresses, so any addresses can be given to them+    set as = zipWith (\i b -> XPubBal {path = [0, i], balance = b}) [0 ..] <$> queries.getBalances as++-- | Whether the page of a question starts somewhere that both layouts+-- know, given a database of version 20: not at a transaction that left+-- the mempool unconfirmed. Version 20 keeps the bodies of those, and+-- starts a page where such a transaction was; the layered layout keeps+-- nothing of them, and its page is empty.+startsKnown :: DatabaseReader -> Question -> IO Bool+startsKnown db question = case startOf question of+  Just (Just (AtTx h)) -> maybe True (not . (.deleted)) <$> Q.getTxRecord db h+  _ -> return True++-- | The questions that two databases answer differently, with the answers+-- of both.+differences :: [Question] -> [Reply] -> [Reply] -> [(Question, Reply, Reply)]+differences asked was now = [(q, a, b) | (q, a, b) <- zip3 asked was now, a /= b]++-- | How much there is in an answer.+size :: Reply -> Int+size = \case+  Refs xs -> length xs+  Outputs xs -> length xs+  Amounts xs -> length xs+  Data xs -> length (catMaybes xs)+  Matches xs -> length xs+  Listed xs -> length xs+  Paths xs -> length xs+  PathOutputs xs -> length xs+  Number n -> fromIntegral n++-- | Where the page of a question starts, if it has a page.+startOf :: Question -> Maybe (Maybe Start)+startOf = \case+  AddressTxs _ limits -> Just limits.start+  AddressesTxs _ limits -> Just limits.start+  AddressUnspents _ limits -> Just limits.start+  AddressesUnspents _ limits -> Just limits.start+  WalletTxs limits -> Just limits.start+  WalletUnspents limits -> Just limits.start+  SetTxs _ limits -> Just limits.start+  SetUnspents _ limits -> Just limits.start+  _ -> Nothing++-- | Questions about a history: pages with small limits and offsets, from the+-- top, from any transaction ever made, whatever became of it, from one that+-- was never made, and from heights up to above the best block; about the+-- addresses of the history, one at a time and several together, with one+-- that was never used and with repetitions; and about the 'wallet'.+questions :: Network -> Ctx -> History -> Int -> Gen [Question]+questions net ctx hist n = vectorOf n question+  where+    question =+      frequency+        [ (4, AddressTxs <$> address <*> limits),+          (4, AddressesTxs <$> several address <*> limits),+          (4, AddressUnspents <$> address <*> limits),+          (4, AddressesUnspents <$> several address <*> limits),+          (1, Balances <$> several address),+          (1, Bodies <$> several hash),+          (1, Numbered . shortNumber <$> hash),+          (1, Pool <$> limits),+          (1, pure WalletBalances),+          (3, WalletTxs <$> limits),+          (3, WalletUnspents <$> limits),+          (1, pure WalletCount),+          (3, SetTxs <$> several address <*> limits),+          (3, SetUnspents <$> several address <*> limits),+          (1, SetCount <$> several address)+        ]+    address = frequency [(12, elements (addressesOf net ctx hist)), (1, elements unused)]+    unused = rights [scriptToAddressBS net ctx (p2pkh 9)]+    hash = frequency [(12, elements (map txHash hist.made)), (1, pure (TxHash (sha256 ("never made" :: ByteString))))]+    several :: Gen a -> Gen [a]+    several one = frequency [(1, pure 0), (12, choose (1, 6))] >>= (`vectorOf` one)+    limits =+      Limits+        <$> frequency [(1, pure 0), (5, choose (1, 6))]+        <*> frequency [(3, pure 0), (2, choose (1, 3))]+        <*> frequency+          [ (3, pure Nothing),+            (6, Just . AtTx <$> hash),+            (2, Just . AtBlock <$> choose (0, best + 2))+          ]+    best :: BlockHeight+    best = case hist.chain of+      (_, node) : _ -> node.height+      [] -> 0++-- | The short number of a transaction in the Blockchain.info API: the first+-- 53 bits of its hash.+shortNumber :: TxHash -> Word64+shortNumber h = either error (`shiftR` 11) (runGet getWord64be (encode h))++-------------------------------------------------------------------------------+-- The chain's column families+-------------------------------------------------------------------------------++-- | What the chain's column families hold, but for what version 20 left+-- there that does not come from the chain: bodies marked as deleted, and+-- balances at zero. The time a transaction was first seen is apart from its+-- body, because a database that never saw the mempool has another.+data Chain = Chain+  { bodies :: ![(ByteString, (BlockRef, Tx, IntMap Prev, Bool))],+    firstSeen :: ![(ByteString, UnixTime)],+    addressTxs :: ![(ByteString, ByteString)],+    addressOutputs :: ![(ByteString, ByteString)],+    unspents :: ![(ByteString, ByteString)],+    balances :: ![(ByteString, BalVal)],+    -- | the inputs of blocks that spend outputs, each with the output that+    -- it spends, whether the database has it under a key of version 22 or+    -- in the record of the transaction that made the output, as the+    -- versions before did: where they are is not what this compares+    spenders :: ![(ByteString, ByteString)]+  }+  deriving (Eq, Show)++chainOf :: DatabaseReader -> IO Chain+chainOf db = do+  txs <- dump db (txCF db.db) >>= mapM (\(k, v) -> (k,) <$> decoded v)+  addressTxs <- dump db (addrTxCF db.db)+  addressOutputs <- dump db (addrOutCF db.db)+  unspents <- dump db (unspentCF db.db)+  balances <- dump db (balanceCF db.db) >>= mapM (\(k, v) -> (k,) <$> decoded v)+  spenders <- dump db (spenderCF db.db)+  let live = [(k, td :: TxData) | (k, td) <- txs, not td.deleted]+  return+    Chain+      { bodies = [(k, (td.block, td.tx, td.prevs, td.rbf)) | (k, td) <- live],+        firstSeen = [(k, td.timestamp) | (k, td) <- live],+        addressTxs,+        addressOutputs,+        unspents,+        balances = filter ((/= def) . snd) balances,+        spenders =+          sort $+            [ (encode (OutPoint h (fromIntegral i)), encode spender)+            | (k, v) <- spenders,+              Right (SpenderKey h _) <- [decode k],+              Right (SpenderVal m) <- [decode v],+              (i, spender) <- IntMap.toList m+            ]+              <> [ (encode (OutPoint (txHash td.tx) (fromIntegral i)), encode spender)+                 | (_, td) <- live,+                   (i, spender) <- IntMap.toList td.spenders+                 ]+      }+  where+    decoded v = either fail return (decode v)++-- | Every key and value of a column family.+dump :: DatabaseReader -> ColumnFamily -> IO [(ByteString, ByteString)]+dump db cf = withIterCF db.db cf $ \iter -> iterFirst iter >> go iter+  where+    go iter =+      iterEntry iter >>= \case+        Nothing -> return []+        Just e -> iterNext iter >> (e :) <$> go iter
+ test/Haskoin/Store/IntegrationSpec.hs view
@@ -0,0 +1,1958 @@+-- |+-- Module      : Haskoin.Store.IntegrationSpec+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Tests of the whole store against regtest chains of Bitcoin Core and Bitcoin+-- Cash Node, with a Redis cache, all in Podman containers.+--+-- The unit tests check pieces against models; these check that the pieces+-- fit: that the store syncs from a real node, that the xpub endpoints find+-- the payments a wallet following the gap would find and no others, and that+-- the Redis cache answers exactly like the database. Each chain gets one node,+-- one Redis and one store. The store is served twice, by a web server with+-- the cache and by one without ('cache' set to 'Nothing'), so both read the+-- same database, mempool times included, and every difference between their+-- answers is a difference made by the cache.+--+-- Every test makes its own xpubs from a fixed seed, sends transactions to+-- chosen derivation paths, mines them, and checks the answers against a+-- ledger that it keeps itself: balance, transactions, unspent outputs, used+-- paths and indices. Then it compares the two web servers on every xpub and+-- Blockchain.info endpoint, with pages, starting points and filters. The cache+-- writer updates Redis in the background, so the comparison is retried for up+-- to 30 seconds before it fails.+--+-- The tests are skipped when Podman cannot run containers, or when+-- @HASKOIN_STORE_NO_PODMAN@ is set. Containers are removed when the tests+-- end, data is kept in tmpfs mounts and in a temporary directory under+-- @\/dev\/shm@ (or @$XDG_RUNTIME_DIR@), and every container has a memory+-- limit. Set @HASKOIN_TEST_LOG@ to see the log of the store.+module Haskoin.Store.IntegrationSpec (spec) where++import Control.Lens ((&), (.~), (?~), (^.))+import Control.Monad (forM_, forever, unless, void, when)+import Data.Aeson (Value (..), eitherDecode, object, toJSON, (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.ByteString.Char8 qualified as C8+import Data.ByteString.Lazy qualified as BL+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize)+import Data.Default (def)+import Data.Either (isRight)+import Data.Foldable (toList)+import Data.HashSet qualified as HashSet+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+import Data.IntMap.Strict qualified as IntMap+import Data.List (nub, sort, sortOn)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.Ord (Down (..))+import Data.Serialize (decode, encode)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Word (Word32, Word64)+import Database.Redis qualified as Redis+import Haskoin+  ( Address (..),+    BloomFlags (..),+    Ctx,+    Network,+    OutPoint (..),+    TxHash (..),+    XPrvKey,+    addrToText,+    addressToScriptBS,+    blockHashToHex,+    bloomCreate,+    bloomInsert,+    deriveXPubKey,+    encodeHex,+    hexToBlockHash,+    hexToTxHash,+    makeXPrvKey,+    marshal,+    marshalJSON,+    prepareContext,+    prvSubKey,+    pubSubKey,+    sha256,+    txHashToHex,+    xPrvWif,+    xPubAddr,+    xPubCompatWitnessAddr,+    xPubExport,+    xPubWitnessAddr,+  )+import Haskoin.Node (LogLevel (..), Logger (..), withConnection, withLogger)+import Haskoin.Store+  ( DeriveType (..),+    Store (..),+    StoreConfig (..),+    WebConfig (..),+    XPubSpec (..),+    runWebSocket,+    withStore,+  )+import Haskoin.Store.Cache+  ( Cache,+    CacheCounters (..),+    Reload (..),+    cacheCounters,+    cacheEvict,+    cacheIsCached,+    cachePrefix,+    cacheRefresh,+    cacheTxData,+    cacheXPubBals,+    xpubId,+  )+import Haskoin.Store.Common (StoreEvent (..))+import Haskoin.Store.Data+  ( Balance (..),+    BlockRef (..),+    Event (..),+    EventCommand (..),+    Except (..),+    Spender (..),+    TxData (..),+    XPubBal (..),+  )+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Fleet.Member (memberTip)+import Haskoin.Store.Integration.Image (testImage)+import Haskoin.Store.Integration.Node+import Haskoin.Store.Integration.Podman+import Haskoin.Store.Integration.Valkey+import NQE (receive, withSubscription)+import Network.Socket qualified as Socket+import Network.WebSockets qualified as WS+import Network.Wreq qualified as Wreq+import Network.Wreq.Types (FormParam ((:=)))+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.Process (readProcessWithExitCode)+import Test.Hspec+import UnliftIO (async, bracket, cancel, link, throwIO, try, tryAny, withAsync, withSystemTempDirectory)+import UnliftIO.Concurrent (threadDelay)++spec :: Spec+spec = do+  usable <- runIO podmanUsable+  describe "Regtest chains in Podman" $ case usable of+    Left why ->+      it "check the store against Bitcoin Core and Bitcoin Cash Node" $+        pendingWith why+    Right () -> prepareContext $ \ctx -> do+      aroundAll (withEnv BitcoinCore ctx) $+        describe "Bitcoin Core" (chainSpec BitcoinCore)+      aroundAll (withEnv BitcoinCashNode ctx) $+        describe "Bitcoin Cash Node" (chainSpec BitcoinCashNode)++chainSpec :: NodeKind -> SpecWith Env+chainSpec kind = do+  it "sees payments up to exactly the gap, and those the gap reaches later" gapTest+  it "does not see the payment 34 addresses past the last used one" noteTest+  it "follows spends and change, confirmed and in the mempool" spendTest+  it "pages mempool transactions of the same second without duplicates" tiesTest+  it "counts every transaction of a multiaddr wallet" multiaddrTest+  it "answers the address, block and transaction endpoints like the node" endpointsTest+  when (kind == BitcoinCore) $ do+    it "derives native and nested SegWit addresses" segwitTest+    it "refuses the wrong kind of address for an xpub" deriveTest+  it "refuses unknown parameters and values it cannot read" strictTest+  it "does not use or fill the cache when asked not to" noCacheTest+  it "recovers from damaged entries and keeps newer data" damagedTest+  it "updates cached xpubs from what changed, and reads them whole when unsure" incrementalTest+  it "catches up with changes that the database has before their events" catchUpTest+  it "keeps the spenders of cached transactions up to date" spenderTest+  it "evicts the least recently used xpubs" evictTest+  it "answers an error in the format that was asked for" errorFormatTest+  it "speaks HTTP as browsers, proxies and long requests need it" httpTest+  it "serves HTTPS, and nothing else, when given a certificate" tlsTest+  it "tells each client of the transactions it chose, and takes up where it left" eventsTest+  it "drops a transaction double-spent in a reorganisation" reorgTest+  it "applies each change once with two stores sharing Redis" onceTest+  it "does not replace what it does not know" unknownTest+  it "leaves the keys of older caches alone" oldKeysTest+  it "answers from its database, and says so, while Redis does not answer" redisDownTest+  it "gives the same answers from every store of a fleet, from one block and one mempool" fleetTest++-------------------------------------------------------------------------------+-- Environment+-------------------------------------------------------------------------------++-- | A node, a store syncing from it with a Redis cache, and the two web+-- servers of the store.+data Env = Env+  { kind :: !NodeKind,+    node :: !Node,+    net :: !Network,+    ctx :: !Ctx,+    store :: !Store,+    -- | base URL of the web server with the cache+    cached :: !String,+    -- | base URL of the web server without the cache+    plain :: !String,+    -- | address of the wallet of the node, to mine to+    miner :: !Text,+    -- | the cache of the store+    redisCache :: !Cache,+    -- | connection to Redis, to look at the keys+    redis :: !Redis.Connection,+    -- | configuration of a store like this one with its database in a+    -- directory, to start another one+    configIn :: FilePath -> StoreConfig,+    -- | the same for a store that is a member of a fleet+    memberIn :: FilePath -> StoreConfig+  }++-- | Initial gap and gap of the store under test, the defaults.+initGap, gap :: Word32+initGap = 20+gap = 32++withEnv :: NodeKind -> Ctx -> (Env -> IO ()) -> IO ()+withEnv kind ctx go = do+  image <- testImage+  withContainer (redisContainer image) $ \redis -> do+    redisPort <- hostPort redis 6379+    let redisURL = "redis://127.0.0.1:" <> show redisPort+    waitRedis redisURL+    writeOldKeys redisURL+    withContainer (nodeContainer kind image) $ \c -> do+      rpcPort <- hostPort c 18443+      p2pPort <- hostPort c 18444+      let node =+            Node+              { kind = kind,+                url = "http://127.0.0.1:" <> show rpcPort <> "/",+                p2p = p2pPort+              }+      waitRpc node+      ensureWallet node+      miner <- getNewAddress node+      -- enough mature coinbases for the wallet to fund every test without+      -- long chains of unconfirmed change+      void $ generateTo node 150 miner+      withRamDir $ \dir -> withTestLogger $ \logger -> do+        let net = nodeNetwork kind+            cfg = storeConfig net ctx logger dir redisURL p2pPort False+        withStore cfg $ \store -> do+          listening $ \(cachedSock, cachedPort) -> listening $ \(plainSock, plainPort) -> do+            let web s = runWebSocket (webConfig logger s)+            conn <- Redis.checkedConnect (either error id (Redis.parseConnectInfo redisURL))+            cache <- maybe (fail "The store has no cache") return store.cache+            withAsync (web store cachedSock) $ \a1 ->+              withAsync (web store {cache = Nothing} plainSock) $ \a2 -> do+                link a1+                link a2+                let env =+                      Env+                        { kind = kind,+                          node = node,+                          net = net,+                          ctx = ctx,+                          store = store,+                          cached = "http://127.0.0.1:" <> show cachedPort,+                          plain = "http://127.0.0.1:" <> show plainPort,+                          miner = miner,+                          redisCache = cache,+                          redis = conn,+                          configIn = \d -> storeConfig net ctx logger d redisURL p2pPort False,+                          memberIn = \d -> storeConfig net ctx logger d redisURL p2pPort True+                        }+                waitSynced env+                go env++-- | A logger that prints the log of the store if @HASKOIN_TEST_LOG@ is set,+-- and discards it otherwise.+withTestLogger :: (Logger -> IO a) -> IO a+withTestLogger f =+  lookupEnv "HASKOIN_TEST_LOG" >>= \case+    Just v | v `notElem` ["", "0"] -> withLogger LevelDebug f+    _ -> f Logger {level = LevelError, source = "", push = \_ -> return ()}++-- | Store with the default gaps, caching every used xpub, with small RocksDB+-- buffers and caches, as a member of a fleet or not.+storeConfig :: Network -> Ctx -> Logger -> FilePath -> String -> Int -> Bool -> StoreConfig+storeConfig net ctx logger dir redisURL p2pPort fleet =+  StoreConfig+    { maxPeers = 1,+      initPeers = ["127.0.0.1:" <> show p2pPort],+      discover = False,+      db = dir,+      net = net,+      redis = Just redisURL,+      ctx = ctx,+      initGap = initGap,+      gap = gap,+      redisMinAddrs = 1,+      redisMinTxs = 1000,+      redisMaxXPubs = 10000,+      noMempool = False,+      wipeMempool = False,+      fleet = fleet,+      syncMempool = True,+      blockWindow = 64,+      mempoolTimeout = 14,+      peerTimeout = 120,+      maxPeerLife = 48 * 3600,+      connect = withConnection,+      stats = Nothing,+      bloom = True,+      maxOpenFiles = Just 256,+      writeBufferSize = Just (16 * 1024 * 1024),+      maxWalSize = Just (64 * 1024 * 1024),+      blockCache = Just (16 * 1024 * 1024),+      logger = logger+    }++webConfig :: Logger -> Store -> WebConfig+webConfig logger store =+  WebConfig+    { host = "127.0.0.1",+      port = 0,+      store = store,+      maxLaggingBlocks = 100,+      maxPendingTxs = 10000,+      minPeers = 0,+      limits = def,+      version = "test",+      noMempool = False,+      stats = Nothing,+      tickerRefresh = 90 * 1000 * 1000,+      tickerURL = "http://127.0.0.1:9/",+      priceHistoryURL = "http://127.0.0.1:9/",+      noXPub = False,+      noBlockchainInfo = False,+      healthCheckInterval = 5,+      tls = Nothing,+      logger = logger+    }++-- | A socket that listens on a port of the loopback interface that the+-- system chose, with that port. Nothing else can take the port between+-- choosing it and serving on it.+listening :: ((Socket.Socket, Int) -> IO a) -> IO a+listening f =+  bracket (Socket.socket Socket.AF_INET Socket.Stream Socket.defaultProtocol) Socket.close $ \s -> do+    Socket.bind s (Socket.SockAddrInet 0 (Socket.tupleToHostAddress (127, 0, 0, 1)))+    Socket.listen s 128+    port <- Socket.socketPort s+    f (s, fromIntegral port)++-------------------------------------------------------------------------------+-- Talking to the store+-------------------------------------------------------------------------------++-- | Status and JSON body of a request. A body that is not JSON, as the+-- plain-text Blockchain.info queries give, is returned as a string.+getJSON :: String -> String -> IO (Int, Value)+getJSON base path = do+  r <- Wreq.getWith opts (base <> path)+  return (r ^. Wreq.responseStatus . Wreq.statusCode, body (r ^. Wreq.responseBody))+  where+    opts = Wreq.defaults & Wreq.checkResponse ?~ (\_ _ -> return ())++-- | Like 'getJSON', posting a form.+postForm :: String -> String -> [(C8.ByteString, Text)] -> IO (Int, Value)+postForm base path params = do+  r <- Wreq.postWith opts (base <> path) [k := v | (k, v) <- params]+  return (r ^. Wreq.responseStatus . Wreq.statusCode, body (r ^. Wreq.responseBody))+  where+    opts = Wreq.defaults & Wreq.checkResponse ?~ (\_ _ -> return ())++body :: BL.ByteString -> Value+body b = either (const (String (cs b))) id (eitherDecode b)++-- | Body of a successful request to the server without the cache.+plainJSON :: Env -> String -> IO Value+plainJSON env path = do+  (code, v) <- getJSON env.plain path+  unless (code == 200) $+    expectationFailure ("GET " <> path <> " answered " <> show code <> ": " <> show v)+  return v++-- | Check that both web servers answer every request in the same way, and+-- successfully. The cache is updated in the background, so try for up to 30+-- seconds.+sameAnswers :: Env -> [String] -> IO ()+sameAnswers env paths = forM_ paths $ \p -> go p (120 :: Int)+  where+    go p n = do+      a <- getJSON env.cached p+      b <- getJSON env.plain p+      if a == b && fst a == 200+        then return ()+        else+          if n <= 0+            then+              expectationFailure $+                "GET "+                  <> p+                  <> "\nstatus with and without the cache: "+                  <> show (fst a, fst b)+                  <> "\nfirst difference: "+                  <> difference "" (snd a) (snd b)+            else threadDelay 250000 >> go p (n - 1)++-- | Where two JSON values first differ, with the two values there.+difference :: String -> Value -> Value -> String+difference at' (Object x) (Object y)+  | KeyMap.keys x /= KeyMap.keys y = at' <> ": keys " <> show (KeyMap.keys x) <> " and " <> show (KeyMap.keys y)+  | otherwise =+      case [ difference (at' <> "." <> show k) v w+           | (k, v) <- KeyMap.toList x,+             Just w <- [KeyMap.lookup k y],+             v /= w+           ] of+        d : _ -> d+        [] -> at' <> ": none"+difference at' (Array x) (Array y)+  | length x /= length y = at' <> ": " <> show (length x) <> " and " <> show (length y) <> " elements"+  | otherwise =+      case [ difference (at' <> "[" <> show i <> "]") v w+           | (i, v, w) <- zip3 [0 :: Int ..] (toList x) (toList y),+             v /= w+           ] of+        d : _ -> d+        [] -> at' <> ": none"+difference at' x y = at' <> ": " <> take 1000 (show x) <> " and " <> take 1000 (show y)++-- | Wait until the store has the best block of the node and every+-- transaction of its mempool, for up to 60 seconds.+waitSynced :: Env -> IO ()+waitSynced env = go (240 :: Int)+  where+    go n = do+      best <- bestBlockHash env.node+      pool <- rawMempool env.node+      r <- tryAny $ do+        (_, b) <- getJSON env.plain "/block/best?notx=true"+        (_, m) <- getJSON env.plain "/mempool"+        let ours = [t | String t <- arr m]+        return (textField "hash" b == Just best && all (`elem` ours) pool)+      case r of+        Right True -> return ()+        _+          | n <= 0 -> do+              (_, b) <- getJSON env.plain "/block/best?notx=true"+              (_, m) <- getJSON env.plain "/mempool"+              let ours = [t | String t <- arr m]+              expectationFailure $+                "The store did not sync to block "+                  <> cs best+                  <> "; its best block is "+                  <> show (textField "hash" b)+                  <> " and it lacks these transactions of the mempool of the node: "+                  <> show (filter (`notElem` ours) pool)+          | otherwise -> threadDelay 250000 >> go (n - 1)++-- | Mine blocks and wait for the store to have them.+mine :: Env -> Int -> IO [Text]+mine env n = do+  hs <- generateTo env.node n env.miner+  waitSynced env+  return hs++-------------------------------------------------------------------------------+-- Test wallets+-------------------------------------------------------------------------------++-- | An xpub of a test, with the key to spend from it.+data XPub = XPub+  { prv :: !XPrvKey,+    spec :: !XPubSpec,+    -- | serialized xpub+    text :: !String,+    -- | derivation path of the first addresses of each chain, by output script+    scripts :: !(Map.Map Text [Word32])+  }++-- | Xpub from a seed, different for each chain.+newXPub :: Env -> String -> DeriveType -> XPub+newXPub env seed deriv =+  XPub+    { prv = prv,+      spec = xspec,+      text = cs (xPubExport env.net env.ctx pub),+      scripts =+        Map.fromList+          [ (scriptHex env (addrOf env x [m, i]), [m, i])+          | m <- [0, 1],+            i <- [0 .. 120]+          ]+    }+  where+    prv = makeXPrvKey (C8.pack ("haskoin-store integration " <> show env.kind <> " " <> seed))+    pub = deriveXPubKey env.ctx prv+    xspec = XPubSpec {key = pub, deriv = deriv}+    x = XPub {prv = prv, spec = xspec, text = "", scripts = Map.empty}++-- | Address at a derivation path, computed with haskoin-core, not the store.+addrOf :: Env -> XPub -> [Word32] -> Address+addrOf env x [m, i] = f (pubSubKey env.ctx (pubSubKey env.ctx x.spec.key m) i)+  where+    f = case x.spec.deriv of+      DeriveNormal -> xPubAddr env.ctx+      DeriveP2SH -> xPubCompatWitnessAddr env.ctx+      DeriveP2WPKH -> xPubWitnessAddr env.ctx+addrOf _ _ p = error ("Bad path: " <> show p)++addrText :: Env -> Address -> Text+addrText env a = fromMaybe (error "Cannot encode address") (addrToText env.net a)++-- | Address at a path of an xpub, as text.+at :: Env -> XPub -> [Word32] -> Text+at env x = addrText env . addrOf env x++scriptHex :: Env -> Address -> Text+scriptHex env a =+  encodeHex (fromMaybe (error "No script for address") (addressToScriptBS env.ctx a))++-- | Private key of the address at a path, as WIF.+wifOf :: Env -> XPub -> [Word32] -> Text+wifOf env x [m, i] = xPrvWif env.net (prvSubKey env.ctx (prvSubKey env.ctx x.prv m) i)+wifOf _ _ p = error ("Bad path: " <> show p)++-- | Parameter that selects the derivation of an xpub.+deriveParam :: XPub -> String+deriveParam x = case x.spec.deriv of+  DeriveNormal -> "legacy"+  DeriveP2SH -> "compat"+  DeriveP2WPKH -> "segwit"++-------------------------------------------------------------------------------+-- Ledger: what the test expects the store to know+-------------------------------------------------------------------------------++-- | Output paid to an address of an xpub.+data Coin = Coin+  { txid :: !Text,+    vout :: !Int,+    path :: ![Word32],+    value :: !Word64+  }+  deriving (Eq, Show)++-- | The outputs paid to an xpub, which of them are spent and by what, and+-- which transactions touch which paths.+data Ledger = Ledger+  { coins :: ![Coin],+    spent :: ![((Text, Int), Text)],+    touched :: ![(Text, [Word32])]+  }++newLedger :: IO (IORef Ledger)+newLedger = newIORef (Ledger [] [] [])++-- | Add a transaction to the ledger of an xpub, reading it from the node.+record :: Env -> XPub -> IORef Ledger -> Text -> IO ()+record env x ref txid = do+  tx <- getRawTx env.node txid+  let ins =+        [ (t, fromIntegral (sats' n))+        | i <- arr ((tx ! "vin")),+          Just t <- [textField "txid" i],+          Just n <- [field "vout" i]+        ]+      outs =+        [ (fromIntegral (sats' n), h, sats v)+        | o <- arr ((tx ! "vout")),+          Just n <- [field "n" o],+          Just v <- [field "value" o],+          Just h <- [textField "hex" =<< field "scriptPubKey" o]+        ]+  modifyIORef' ref $ \l ->+    let mine' = [c | c <- l.coins, (c.txid, c.vout) `elem` ins]+        new =+          [ Coin {txid = txid, vout = n, path = p, value = v}+          | (n, h, v) <- outs,+            Just p <- [Map.lookup h x.scripts]+          ]+     in Ledger+          { coins = l.coins <> new,+            spent = l.spent <> [((c.txid, c.vout), txid) | c <- mine'],+            touched =+              l.touched+                <> nub ([(txid, c.path) | c <- mine'] <> [(txid, c.path) | c <- new])+          }+  where+    -- integer JSON numbers+    sats' (Number n) = round n :: Integer+    sats' v = error ("Not a number: " <> show v)++-- | Remove a transaction from a ledger, as if it never happened.+forget :: IORef Ledger -> Text -> IO ()+forget ref txid = modifyIORef' ref $ \l ->+  Ledger+    { coins = [c | c <- l.coins, c.txid /= txid],+      spent = [s | s@(_, t) <- l.spent, t /= txid],+      touched = [t | t@(h, _) <- l.touched, h /= txid]+    }++-- | Unspent output at a path.+coinAt :: Ledger -> [Word32] -> Coin+coinAt l p =+  case [c | c <- l.coins, c.path == p, (c.txid, c.vout) `notElem` map fst l.spent] of+    c : _ -> c+    [] -> error ("No unspent output at " <> show p)++-- | Last used index that a wallet following the gap reaches from the used+-- indices of a chain, starting from those in the first @start@ addresses, or+-- -1 if it reaches none.+reach :: Word32 -> [Int] -> Int+reach start used = go first+  where+    first = maximum ((-1) : filter (< fromIntegral start) used)+    go l = case filter (\u -> u > l && u <= l + fromIntegral gap) used of+      [] -> l+      us -> go (maximum us)++-- | What the store should answer for an xpub.+data Expect = Expect+  { balance :: !Word64,+    txids :: ![Text],+    utxos :: ![(Text, Int)],+    used :: ![[Word32]],+    external :: !Int,+    change :: !Int+  }+  deriving (Eq, Show)++expect :: Ledger -> Expect+expect l =+  Expect+    { balance = sum [c.value | c <- live],+      txids = sort (nub [t | (t, p) <- l.touched, p `elem` visible]),+      utxos = sort [(c.txid, c.vout) | c <- live],+      used = sort visible,+      external = maximum (0 : [fromIntegral i | [0, i] <- visible]),+      change = maximum (0 : [fromIntegral i | [1, i] <- visible])+    }+  where+    paths = nub (map snd l.touched)+    ext = [fromIntegral i | [0, i] <- paths]+    chg = [fromIntegral i | [1, i] <- paths]+    lastExt = reach initGap ext+    lastChg = reach 0 chg+    visible+      | lastExt < 0 = []+      | otherwise =+          [ p+          | p@[m, i] <- paths,+            fromIntegral i <= if m == 0 then lastExt else lastChg+          ]+    live =+      [ c+      | c <- l.coins,+        c.path `elem` visible,+        (c.txid, c.vout) `notElem` map fst l.spent+      ]++-------------------------------------------------------------------------------+-- Checks+-------------------------------------------------------------------------------++-- | Check what the store says about an xpub against the ledger, then that+-- the cache says the same on every endpoint, and that the xpub is cached.+checkXPub :: Env -> XPub -> IORef Ledger -> IO ()+checkXPub env x ref = do+  e <- expect <$> readIORef ref+  let b = "/xpub/" <> x.text+      q = "?derive=" <> deriveParam x+  summary <- plainJSON env (b <> q)+  let bal = (summary ! "balance")+      idx = (summary ! "indices")+  (num "confirmed" bal + num "unconfirmed" bal) `shouldBe` e.balance+  num "utxo" bal `shouldBe` fromIntegral (length e.utxos)+  num "external" idx `shouldBe` fromIntegral e.external+  num "change" idx `shouldBe` fromIntegral e.change+  txs <- arr <$> plainJSON env (b <> "/transactions" <> q)+  let ts = mapMaybe (textField "txid") txs+  ts `shouldBe` nub ts+  sort ts `shouldBe` e.txids+  newestFirst (map blockOf txs)+  us <- arr <$> plainJSON env (b <> "/unspent" <> q)+  sort+    [ (t, fromIntegral (num "index" u'))+    | u <- us,+      Just u' <- [field "unspent" u],+      Just t <- [textField "txid" u']+    ]+    `shouldBe` e.utxos+  bs <- arr <$> plainJSON env (b <> "/balances" <> q)+  sort+    [ p+    | xb <- bs,+      Just bb <- [field "balance" xb],+      num "txs" bb > 0,+      Just (Array p') <- [field "path" xb],+      let p = map (fromIntegral . numV) (toList p')+    ]+    `shouldBe` e.used+  let heights = nub [show h | BlockRef h _ <- map blockOf txs]+  unless (null e.used) $ do+    _ <- getJSON env.cached (b <> "/balances" <> q)+    waitCached env x+  sameAnswers env (xpubPaths x ts heights)+  unless (null e.used) $ do+    -- every body of the xpub is in Redis, and is the body of the database,+    -- spenders included+    let hs = map (\t -> fromMaybe (error "bad txid") (hexToTxHash t)) e.txids+    waitFor "cached bodies" $ do+      fromCache <- cacheTxData env.redisCache hs+      fromDB <- L.withView env.store.db (`L.getManyTxData` hs)+      return (fromCache == fromDB && all isJust fromCache)++-- | Requests about an xpub to compare between the servers. The balances come+-- first, as they are what puts an xpub in the cache.+xpubPaths :: XPub -> [Text] -> [String] -> [String]+xpubPaths x ts heights =+  [ b <> "/balances" <> q,+    b <> q,+    t,+    t <> "&limit=1",+    t <> "&limit=2&offset=1",+    t <> "&limit=3&offset=2",+    b <> "/transactions/full" <> q <> "&limit=4",+    u,+    u <> "&limit=1",+    u <> "&limit=2&offset=1",+    b <> q <> "&nocache=true",+    t <> "&nocache=true",+    u <> "&nocache=true"+  ]+    <> concat+      [ [ t <> "&height=" <> s <> "&limit=2",+          t <> "&height=" <> s <> "&limit=2&offset=1",+          u <> "&height=" <> s <> "&limit=2"+        ]+      | s <- map cs ts <> heights+      ]+    <> binfo+  where+    b = "/xpub/" <> x.text+    q = "?derive=" <> deriveParam x+    t = b <> "/transactions" <> q+    u = b <> "/unspent" <> q+    active = case x.spec.deriv of+      DeriveNormal -> "active="+      DeriveP2SH -> "activeP2SH="+      DeriveP2WPKH -> "activeBech32="+    m = "/blockchain/multiaddr?" <> active <> x.text+    binfo =+      [ m,+        m <> "&n=1",+        m <> "&n=2&offset=1",+        m <> "&filter=2",+        m <> "&filter=5",+        m <> "&filter=7",+        "/blockchain/unspent?" <> active <> x.text,+        "/blockchain/balance?" <> active <> x.text,+        "/blockchain/rawaddr/" <> x.text <> "?derive=" <> deriveParam x,+        "/blockchain/rawaddr/" <> x.text <> "?derive=" <> deriveParam x <> "&limit=2&offset=1",+        "/blockchain/xpub/" <> x.text <> "?derive=" <> deriveParam x+      ]++-- | Block of a transaction or unspent output in a JSON answer.+blockOf :: Value -> BlockRef+blockOf v = case field "block" v of+  Just b+    | Just m <- field "mempool" b -> MemRef (fromIntegral (numV m))+    | Just h <- field "height" b,+      Just p <- field "position" b ->+        BlockRef (fromIntegral (numV h)) (fromIntegral (numV p))+  _ -> error ("No block in " <> show v)++-- | Check that blocks come newest first: the mempool, then by height and+-- position.+newestFirst :: [BlockRef] -> IO ()+newestFirst bs = bs `shouldBe` sortOn Down bs++-- | Field of a JSON object, or null.+(!) :: Value -> Text -> Value+v ! k = fromMaybe Null (field k v)++arr :: Value -> [Value]+arr (Array a) = toList a+arr v = error ("Not an array: " <> take 500 (show v))++numV :: Value -> Integer+numV (Number n) = round n+numV v = error ("Not a number: " <> show v)++num :: Text -> Value -> Word64+num k v = maybe (error ("No " <> cs k <> " in " <> take 500 (show v))) (fromIntegral . numV) (field k v)++-- | Send from the wallet of the node to paths of an xpub, in one+-- transaction, and record it.+pay :: Env -> XPub -> IORef Ledger -> [([Word32], Word64)] -> IO Text+pay env x ref outs = do+  txid <- sendMany env.node [(at env x p, v) | (p, v) <- outs]+  record env x ref txid+  return txid++-- | Spend outputs of an xpub, signing with its keys, and record it.+spend :: Env -> XPub -> IORef Ledger -> [Coin] -> [(Text, Word64)] -> IO Text+spend env x ref cs' outs = do+  let ins = [object ["txid" .= c.txid, "vout" .= c.vout] | c <- cs']+      prevs =+        [ object+            [ "txid" .= c.txid,+              "vout" .= c.vout,+              "scriptPubKey" .= scriptHex env (addrOf env x c.path),+              "amount" .= btc c.value+            ]+        | c <- cs'+        ]+      wifs = nub [wifOf env x c.path | c <- cs']+  txid <- signAndSend env ins outs wifs prevs+  record env x ref txid+  return txid++-- | Create, sign with keys and send a transaction. Returns its hash.+signAndSend :: Env -> [Value] -> [(Text, Word64)] -> [Text] -> [Value] -> IO Text+signAndSend env ins outs wifs prevs = do+  raw <- rpc env.node "createrawtransaction" [toJSON ins, object [Key.fromText a .= btc v | (a, v) <- outs]]+  signed <- rpc env.node "signrawtransactionwithkey" [raw, toJSON wifs, toJSON prevs]+  unless (field "complete" signed == Just (Bool True)) $+    expectationFailure ("Could not sign: " <> show signed)+  hex <- maybe (fail "No hex") return (field "hex" signed)+  rpc env.node "sendrawtransaction" [hex] >>= \case+    String t -> return t+    v -> fail ("Unexpected answer to sendrawtransaction: " <> show v)++-------------------------------------------------------------------------------+-- Tests+-------------------------------------------------------------------------------++-- | Payments at the edge of the gap. Index 37 is 32 past index 5, the last+-- used one before it, so it is seen; index 70 is 33 past index 37, so it is+-- not. Then a payment to index 69, 32 past index 37, makes index 70 be seen+-- too: the cache has to derive and read further on its own to find it.+gapTest :: Env -> IO ()+gapTest env = do+  let x = newXPub env "gap" DeriveNormal+  ref <- newLedger+  void $+    pay env x ref $+      [([0, 0], 100000), ([0, 1], 200000), ([0, 2], 300000), ([0, 5], 400000)]+        <> [([0, 37], 500000), ([0, 70], 600000)]+  void $ mine env 1+  e1 <- expect <$> readIORef ref+  e1.used `shouldBe` [[0, 0], [0, 1], [0, 2], [0, 5], [0, 37]]+  e1.balance `shouldBe` 1500000+  checkXPub env x ref+  void $ pay env x ref [([0, 69], 700000)]+  waitSynced env+  e2 <- expect <$> readIORef ref+  e2.used `shouldBe` [[0, 0], [0, 1], [0, 2], [0, 5], [0, 37], [0, 69], [0, 70]]+  checkXPub env x ref+  void $ mine env 1+  checkXPub env x ref++-- | The wallet of the Blockchain.com comparison: receive indices 0 to 35+-- used, and a payment to index 69, 34 past index 35. With a gap of 32 it is+-- not seen, as Blockchain.com does not see it.+noteTest :: Env -> IO ()+noteTest env = do+  let x = newXPub env "blockchain.com" DeriveNormal+  ref <- newLedger+  void $ pay env x ref $ [([0, i], 10000 + fromIntegral i) | i <- [0 .. 35]] <> [([0, 69], 280348)]+  void $ mine env 1+  e <- expect <$> readIORef ref+  e.external `shouldBe` 35+  e.balance `shouldBe` sum [10000 + i | i <- [0 .. 35]]+  checkXPub env x ref+  ma <- plainJSON env ("/blockchain/multiaddr?active=" <> x.text)+  let w = (ma ! "wallet")+  num "final_balance" w `shouldBe` e.balance+  num "n_tx" w `shouldBe` 1+  case arr ((ma ! "addresses")) of+    [a] -> do+      num "account_index" a `shouldBe` 36+      num "change_index" a `shouldBe` 0+      num "n_tx" a `shouldBe` 1+    as -> expectationFailure ("Expected one xpub, got " <> show as)++-- | Spends from two addresses of an xpub at once, with change, then a spend+-- of the change while it is still in the mempool.+spendTest :: Env -> IO ()+spendTest env = do+  let x = newXPub env "spend" DeriveNormal+  ref <- newLedger+  void $ pay env x ref [([0, 0], 1000000), ([0, 1], 2000000)]+  void $ mine env 1+  checkXPub env x ref+  out1 <- getNewAddress env.node+  l1 <- readIORef ref+  void $+    spend env x ref [coinAt l1 [0, 0], coinAt l1 [0, 1]] $+      [(out1, 1500000), (at env x [1, 0], 1490000)]+  waitSynced env+  checkXPub env x ref+  out2 <- getNewAddress env.node+  l2 <- readIORef ref+  void $ spend env x ref [coinAt l2 [1, 0]] [(out2, 500000), (at env x [1, 1], 980000)]+  waitSynced env+  e <- expect <$> readIORef ref+  e.balance `shouldBe` 980000+  e.change `shouldBe` 1+  checkXPub env x ref+  void $ mine env 1+  checkXPub env x ref++-- | Mempool transactions sent one after the other, most of them in the same+-- second, touching the same addresses several times, one of them paying+-- three addresses of the xpub. The store orders those of the same second by+-- hash; it used to list some twice. Then page through the transactions one+-- at a time, as 'streamThings' does, and check that the pages add up.+tiesTest :: Env -> IO ()+tiesTest env = do+  let x = newXPub env "ties" DeriveNormal+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000)]+  void $ mine env 1+  void $ pay env x ref [([0, 1], 110000), ([0, 2], 120000), ([0, 3], 130000)]+  void $ pay env x ref [([0, 2], 140000), ([0, 4], 150000)]+  void $ pay env x ref [([0, 1], 160000), ([0, 4], 170000)]+  void $ pay env x ref [([0, 3], 180000)]+  void $ pay env x ref [([0, 0], 190000), ([0, 1], 200000)]+  waitSynced env+  checkXPub env x ref+  forM_ [env.plain, env.cached] (pagesAddUp x)+  void $ mine env 1+  checkXPub env x ref+  forM_ [env.plain, env.cached] (pagesAddUp x)+  where+    pagesAddUp x base = do+      let t = "/xpub/" <> x.text <> "/transactions?derive=legacy"+      (_, whole) <- getJSON base t+      let go acc start = do+            let p = case start of+                  Nothing -> t <> "&limit=1"+                  Just s -> t <> "&limit=1&offset=1&height=" <> cs s+            (_, page) <- getJSON base p+            case mapMaybe (textField "txid") (arr page) of+              [] -> return (reverse acc)+              [h] -> go (h : acc) (Just h)+              hs -> fail ("Page of more than one: " <> show hs)+      paged <- go [] Nothing+      paged `shouldBe` mapMaybe (textField "txid") (arr whole)++-- | A multiaddr wallet of two xpubs and an address, with a transaction that+-- touches all three. The wallet counts every transaction once, whatever the+-- page size, and @onlyShow@ narrows the count.+multiaddrTest :: Env -> IO ()+multiaddrTest env = do+  let xa = newXPub env "multi a" DeriveNormal+      xb = newXPub env "multi b" DeriveNormal+      xq = newXPub env "multi address" DeriveNormal+      q = at env xq [0, 0]+  ra <- newLedger+  rb <- newLedger+  rq <- newLedger+  t1 <- sendMany env.node [(at env xa [0, 0], 100000), (at env xb [0, 0], 200000), (q, 300000)]+  forM_ [(xa, ra), (xb, rb), (xq, rq)] $ \(x, r) -> record env x r t1+  void $ pay env xa ra [([0, 1], 110000)]+  void $ pay env xb rb [([0, 1], 120000)]+  void $ mine env 1+  void $ pay env xq rq [([0, 0], 130000)]+  waitSynced env+  forM_ [(xa, ra), (xb, rb)] $ uncurry (checkXPub env)+  let active = xa.text <> "|" <> xb.text <> "|" <> cs q+      m = "/blockchain/multiaddr?active=" <> active+  w1 <- plainJSON env (m <> "&n=1")+  length (arr ((w1 ! "txs"))) `shouldBe` 1+  let wallet1 = (w1 ! "wallet")+  num "n_tx" wallet1 `shouldBe` 4+  num "n_tx_filtered" wallet1 `shouldBe` 4+  num "final_balance" wallet1 `shouldBe` (100000 + 200000 + 300000 + 110000 + 120000 + 130000)+  w2 <- plainJSON env (m <> "&onlyShow=" <> cs q)+  num "n_tx" ((w2 ! "wallet")) `shouldBe` 2+  length (arr ((w2 ! "txs"))) `shouldBe` 2+  w3 <- plainJSON env (m <> "&n=2&offset=1")+  length (arr ((w3 ! "txs"))) `shouldBe` 2+  forM_ (arr ((w1 ! "addresses"))) $ \a ->+    case textField "address" a of+      Just t+        | t == cs xa.text -> num "account_index" a `shouldBe` 2+        | t == cs xb.text -> num "account_index" a `shouldBe` 2+        | t == q -> num "final_balance" a `shouldBe` 430000+      _ -> return ()+  sameAnswers env $+    [ m,+      m <> "&n=1",+      m <> "&n=2&offset=1",+      m <> "&onlyShow=" <> cs q,+      m <> "&onlyShow=" <> xa.text,+      m <> "&filter=1",+      m <> "&filter=2",+      m <> "&filter=5",+      m <> "&filter=7",+      "/blockchain/unspent?active=" <> active,+      "/blockchain/balance?active=" <> active+    ]+  -- the request as the Blockchain.com wallet sends it: a form+  posted <-+    postForm+      env.cached+      "/blockchain/multiaddr"+      [("active", cs active), ("n", "10"), ("offset", "0"), ("format", "json")]+  got <- getJSON env.plain (m <> "&n=10&offset=0")+  posted `shouldBe` got++-- | Endpoints that do not involve xpubs, checked against the node.+endpointsTest :: Env -> IO ()+endpointsTest env = do+  let x = newXPub env "endpoints" DeriveNormal+      a = at env x [0, 0]+  ref <- newLedger+  t1 <- pay env x ref [([0, 0], 400000)]+  void $ mine env 1+  t2 <- pay env x ref [([0, 0], 50000)]+  waitSynced env+  best <- bestBlockHash env.node+  height <- rpc env.node "getblockcount" []+  b <- plainJSON env "/block/best?notx=true"+  textField "hash" b `shouldBe` Just best+  field "height" b `shouldBe` Just height+  hs <- arr <$> plainJSON env ("/block/height/" <> show (numV height) <> "?notx=true")+  map (textField "hash") hs `shouldBe` [Just best]+  tx <- plainJSON env ("/transaction/" <> cs t1)+  textField "txid" tx `shouldBe` Just t1+  rawNode <- rpc env.node "getrawtransaction" [String t1, Bool False]+  raw <- plainJSON env ("/transaction/" <> cs t1 <> "/raw")+  field "result" raw `shouldBe` Just rawNode+  bal <- plainJSON env ("/address/" <> cs a <> "/balance")+  num "confirmed" bal `shouldBe` 400000+  num "unconfirmed" bal `shouldBe` 50000+  num "txs" bal `shouldBe` 2+  ats <- arr <$> plainJSON env ("/address/" <> cs a <> "/transactions")+  mapMaybe (textField "txid") ats `shouldBe` [t2, t1]+  aus <- arr <$> plainJSON env ("/address/" <> cs a <> "/unspent")+  length aus `shouldBe` 2+  pool <- arr <$> plainJSON env "/mempool"+  (String t2 `elem` pool) `shouldBe` True+  lb <- plainJSON env "/blockchain/latestblock"+  textField "hash" lb `shouldBe` Just best+  (_, count) <- getJSON env.plain "/blockchain/q/getblockcount"+  count `shouldBe` height+  rt <- plainJSON env ("/blockchain/rawtx/" <> cs t1)+  textField "hash" rt `shouldBe` Just t1+  rb <- plainJSON env ("/blockchain/rawblock/" <> cs best)+  textField "hash" rb `shouldBe` Just best+  ab <- plainJSON env ("/blockchain/q/addressbalance/" <> cs a)+  ab `shouldBe` Number 450000+  ra <- plainJSON env ("/blockchain/rawaddr/" <> cs a)+  num "n_tx" ra `shouldBe` 2+  num "final_balance" ra `shouldBe` 450000+  peers <- arr <$> plainJSON env "/peers"+  length peers `shouldBe` 1+  sameAnswers env $+    [ "/block/best",+      "/block/height/" <> show (numV height),+      "/transaction/" <> cs t1,+      "/transaction/" <> cs t2,+      "/address/" <> cs a <> "/balance",+      "/address/" <> cs a <> "/transactions",+      "/address/" <> cs a <> "/transactions/full",+      "/address/" <> cs a <> "/unspent",+      "/mempool",+      "/blockchain/rawaddr/" <> cs a,+      "/blockchain/multiaddr?active=" <> cs a,+      "/blockchain/unspent?active=" <> cs a,+      "/blockchain/latestblock",+      "/blockchain/unconfirmed-transactions"+    ]++-- | Xpubs of native and nested SegWit addresses, with a spend from a native+-- SegWit address. Only Bitcoin has SegWit.+segwitTest :: Env -> IO ()+segwitTest env = do+  let xs = newXPub env "segwit" DeriveP2WPKH+      xc = newXPub env "compat" DeriveP2SH+  rs <- newLedger+  rc <- newLedger+  void $ pay env xs rs [([0, 0], 300000), ([0, 1], 400000)]+  void $ pay env xc rc [([0, 0], 500000), ([0, 3], 600000)]+  void $ mine env 1+  checkXPub env xs rs+  checkXPub env xc rc+  out <- getNewAddress env.node+  l <- readIORef rs+  void $ spend env xs rs [coinAt l [0, 0]] [(out, 100000), (at env xs [1, 0], 190000)]+  waitSynced env+  checkXPub env xs rs+  void $ mine env 1+  checkXPub env xs rs+  sameAnswers env ["/blockchain/multiaddr?activeBech32=" <> xs.text <> "&activeP2SH=" <> xc.text]++-- | A parameter that an endpoint does not accept, or a value that it cannot+-- read, must give 400 with a message that names it, rather than an answer+-- that ignores it; and a value it can read must still be accepted.+strictTest :: Env -> IO ()+strictTest env = do+  let x = newXPub env "strict" DeriveNormal+      a = at env x [0, 0]+  (_, best) <- getJSON env.plain "/block/best"+  coinbase <- case best ! "tx" of+    Array ts | String t : _ <- toList ts -> return t+    _ -> fail "No coinbase"+  let refused =+        [ ("/block/best?limt=5", "Unknown parameter: limt"),+          ("/block/best?notx=yes", "Invalid value for parameter notx: yes"),+          ("/block/best?pretty=maybe", "Invalid value for parameter pretty"),+          ("/mempool?limit=-1", "Invalid value for parameter limit"),+          ("/address/" <> cs a <> "/transactions?offset=x", "Invalid value for parameter offset"),+          ("/xpub/" <> x.text <> "?derive=Legacy", "Invalid value for parameter derive"),+          ("/blockchain/latestblock?txidindex=1", "Invalid value for parameter txidindex"),+          ("/blockchain/latestblock?format=hex", "Invalid value for parameter format"),+          ("/blockchain/latestblock?pretty=true", "Unknown parameter: pretty"),+          ("/blockchain/rawtx/" <> cs coinbase <> "?format=xml", "Invalid value for parameter format"),+          ("/blockchain/multiaddr?active=" <> x.text <> "&filter=4", "Invalid value for parameter filter"),+          ("/blockchain/unspent?active=" <> cs a <> "&confirmations=-2", "Invalid value for parameter confirmations"),+          ("/blockchain/rawaddr/" <> cs a <> "?limit=ten", "Invalid value for parameter limit")+        ]+  forM_ refused $ \(p, msg) -> do+    (code, v) <- getJSON env.plain p+    (p, code, textField "error" v) `shouldBe` (p, 400, Just "user-error")+    fmap (T.isInfixOf msg) (textField "message" v) `shouldBe` Just True+  -- refused before anything is done: the body is not even read+  (code, v) <- postForm env.plain "/transactions" [("hex", "00")]+  (code, textField "message" v) `shouldBe` (400, Just "Unknown parameter: hex")+  forM_+    [ "/block/best?notx=true&pretty=true",+      "/blockchain/latestblock?format=json&txidindex=true",+      "/xpub/" <> x.text <> "?derive=normal&nocache=false"+    ]+    $ \p -> do+      (code', _) <- getJSON env.plain p+      (p, code') `shouldBe` (p, 200)++-- | An xpub asked about with the wrong @derive@ must give an error that names+-- the right one, from the database and from the cache, and from every xpub+-- endpoint; an xpub unused with every kind must still give an empty wallet.+deriveTest :: Env -> IO ()+deriveTest env = do+  let x = newXPub env "derive" DeriveP2WPKH+      fresh = newXPub env "derive-fresh" DeriveP2WPKH+  ref <- newLedger+  void $ pay env x ref [([0, 0], 300000)]+  void $ mine env 1+  checkXPub env x ref+  let b = "/xpub/" <> x.text+      ends = ["", "/transactions", "/transactions/full", "/balances", "/unspent"]+      wrong = [b <> e <> "?derive=" <> d | e <- ends, d <- ["legacy", "normal", "compat"]]+  forM_ (wrong <> ["/blockchain/rawaddr/" <> x.text <> "?derive=normal"]) $ \p ->+    forM_ [env.plain, env.cached] $ \base -> do+      (code, v) <- getJSON base p+      (p, code) `shouldBe` (p, 400)+      textField "error" v `shouldBe` Just "user-error"+      fmap (T.isInfixOf "derive=segwit") (textField "message" v) `shouldBe` Just True+  -- legacy is the default, and normal its old name+  forM_ ["", "?derive=legacy", "?derive=normal"] $ \q -> do+    (code, v) <- getJSON env.plain ("/xpub/" <> fresh.text <> q)+    code `shouldBe` 200+    num "confirmed" (v ! "balance") `shouldBe` 0++-- | A transaction confirmed in a block that a reorganisation removes, and+-- double-spent in the new chain, must disappear from the store and from the+-- cache.+--+-- Bitcoin Cash Node cannot mine a chosen transaction (@generateblock@), so+-- the double spend is set up with coinbase maturity: transaction A spends a+-- coinbase in the first block where it may, block X. Invalidating the parent+-- of X makes the coinbase immature again, so the node drops A from its+-- mempool. One block later the coinbase is mature, B spends it to another+-- address, and two more blocks make the new chain longer than the old one.+reorgTest :: Env -> IO ()+reorgTest env = do+  let x = newXPub env "reorg" DeriveNormal+      k = newXPub env "coinbase" DeriveNormal+  ref <- newLedger+  void $ mine env 1+  [cbBlock] <- generateTo env.node 1 (at env k [0, 0])+  block <- rpc env.node "getblock" [String cbBlock]+  cbTxid <- case field "tx" block of+    Just (Array ts) | String t : _ <- toList ts -> return t+    _ -> fail "No coinbase"+  cbTx <- getRawTx env.node cbTxid+  cbValue <- case field "vout" cbTx of+    Just (Array os) | o : _ <- toList os, Just v <- field "value" o -> return (sats v)+    _ -> fail "No coinbase output"+  let coinbaseSpend to amount =+        signAndSend+          env+          [object ["txid" .= cbTxid, "vout" .= (0 :: Int)]]+          [(to, amount)]+          [wifOf env k [0, 0]]+          [ object+              [ "txid" .= cbTxid,+                "vout" .= (0 :: Int),+                "scriptPubKey" .= scriptHex env (addrOf env k [0, 0]),+                "amount" .= btc cbValue+              ]+          ]+  -- 99 blocks on top: the coinbase may be spent in the next block+  void $ mine env 99+  txA <- coinbaseSpend (at env x [0, 0]) (cbValue - 100000)+  record env x ref txA+  [blockX] <- mine env 1+  checkXPub env x ref+  header <- rpc env.node "getblockheader" [String blockX]+  parent <- maybe (fail "No parent") return (textField "previousblockhash" header)+  void $ rpc env.node "invalidateblock" [String parent]+  pool <- rawMempool env.node+  (txA `elem` pool) `shouldBe` False+  -- mined to the address of the invalidated parent, in the same second and+  -- without transactions, the block would be the parent again, which the+  -- node refuses as known to be invalid+  other <- getNewAddress env.node+  void $ generateTo env.node 1 other+  txB <- coinbaseSpend (at env x [0, 1]) (cbValue - 200000)+  void $ mine env 2+  forget ref txA+  record env x ref txB+  e <- expect <$> readIORef ref+  e.txids `shouldBe` [txB]+  checkXPub env x ref++-------------------------------------------------------------------------------+-- Cache+-------------------------------------------------------------------------------++-- | Try a check for up to 30 seconds.+waitFor :: String -> IO Bool -> IO ()+waitFor what check = go (120 :: Int)+  where+    go n =+      check >>= \case+        True -> return ()+        False+          | n <= 0 -> expectationFailure ("Timed out waiting for " <> what)+          | otherwise -> threadDelay 250000 >> go (n - 1)++-- | Wait until the cache has an xpub.+waitCached :: Env -> XPub -> IO ()+waitCached env x = waitFor ("xpub " <> x.text <> " to be cached") (isCached env x)++-- | Whether an xpub is cached.+isCached :: Env -> XPub -> IO Bool+isCached env x = cacheIsCached env.redisCache x.spec++-- | Key of the entry of an xpub in Redis.+xpubKey :: Env -> XPub -> C8.ByteString -> C8.ByteString+xpubKey env x suffix = cachePrefix env.net <> "x:" <> xpubId x.spec <> ":" <> suffix++-- | Run Redis commands of a test.+redisRun :: Env -> Redis.Redis (Either Redis.Reply a) -> IO a+redisRun env f = Redis.runRedis env.redis f >>= either (fail . show) return++-- | Keys of the cache of older versions of the store, which the new cache+-- must not touch.+oldKeys :: [(C8.ByteString, C8.ByteString)]+oldKeys =+  [ ("head", "old head"),+    ("lock", "old lock"),+    ("mempool", "old mempool"),+    ("b\x01xpub", "old balances"),+    ("t\x01xpub", "old transactions"),+    ("a\x02address", "old address")+  ]++writeOldKeys :: String -> IO ()+writeOldKeys url = do+  conn <- Redis.checkedConnect (either error id (Redis.parseConnectInfo url))+  void . Redis.runRedis conn $ mapM_ (uncurry Redis.set) oldKeys+  Redis.disconnect conn++-- | Queries with @nocache=true@ must neither read the cache nor put the xpub+-- in it; a query without it then caches the xpub.+noCacheTest :: Env -> IO ()+noCacheTest env = do+  let x = newXPub env "nocache" DeriveNormal+      b = "/xpub/" <> x.text+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000), ([0, 1], 100000)]+  void $ mine env 1+  forM_ ["/transactions", "/transactions/full", "/unspent", "/balances", ""] $ \p -> do+    (code, _) <- getJSON env.cached (b <> p <> "?nocache=true")+    code `shouldBe` 200+  threadDelay 1000000+  isCached env x `shouldReturn` False+  checkXPub env x ref++-- | A cached entry that cannot be decoded is cached again instead of failing+-- the request, and a reload does not replace an entry stamped with a higher+-- block, as one written by a store that is ahead would be.+damagedTest :: Env -> IO ()+damagedTest env = do+  let x = newXPub env "damaged" DeriveNormal+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000), ([0, 1], 200000)]+  void $ mine env 1+  checkXPub env x ref+  void . redisRun env $ Redis.set (xpubKey env x "bal") "garbage"+  checkXPub env x ref+  bal <- redisRun env $ Redis.get (xpubKey env x "bal")+  (decode <$> bal :: Maybe (Either String [XPubBal])) `shouldSatisfy` maybe False isRight+  void . redisRun env $ Redis.hset (xpubKey env x "meta") (pure ("height", "999999999"))+  cacheRefresh env.redisCache x.spec HashSet.empty `shouldReturn` Behind+  void . redisRun env $ Redis.hset (xpubKey env x "meta") (pure ("height", "0"))+  cacheRefresh env.redisCache x.spec HashSet.empty `shouldReturn` Reloaded+  checkXPub env x ref+  -- a reload writes only differences, computed from what Redis has, so+  -- members that should not be there go, and the generation goes up+  gen0 <- redisRun env $ Redis.hget (xpubKey env x "meta") "gen"+  void . redisRun env $ Redis.zadd (xpubKey env x "txs") [(1, "not a transaction")]+  void . redisRun env $ Redis.zadd (xpubKey env x "utxo") [(1, "not an output")]+  cacheRefresh env.redisCache x.spec HashSet.empty `shouldReturn` Reloaded+  redisRun env (Redis.zscore (xpubKey env x "txs") "not a transaction") `shouldReturn` Nothing+  redisRun env (Redis.zscore (xpubKey env x "utxo") "not an output") `shouldReturn` Nothing+  gen1 <- redisRun env $ Redis.hget (xpubKey env x "meta") "gen"+  (read . C8.unpack <$> gen1) `shouldBe` ((+ (1 :: Int)) . read . C8.unpack <$> gen0)+  checkXPub env x ref++-- | Changes of a cached xpub are applied from the transactions that make+-- them, without a full reload: the time of the last full reload in the entry+-- stays, and the answers stay those of the database. When Redis has a balance+-- that no changed transaction explains, a change was missed, and the next+-- reload reads the xpub whole, which repairs it.+incrementalTest :: Env -> IO ()+incrementalTest env = do+  let x = newXPub env "incremental" DeriveNormal+      meta k = redisRun env (Redis.hget (xpubKey env x "meta") k)+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000), ([0, 1], 200000), ([1, 0], 300000)]+  void $ mine env 1+  checkXPub env x ref+  full0 <- meta "full"+  full0 `shouldSatisfy` isJust+  meta "ver" `shouldReturn` Just "3"+  -- a payment in the mempool, its confirmation, and a spend+  void $ pay env x ref [([0, 2], 400000)]+  waitSynced env+  checkXPub env x ref+  void $ mine env 1+  checkXPub env x ref+  out <- getNewAddress env.node+  l <- readIORef ref+  void $ spend env x ref [coinAt l [0, 1]] [(out, 50000), (at env x [1, 1], 140000)]+  waitSynced env+  checkXPub env x ref+  void $ mine env 1+  checkXPub env x ref+  meta "full" `shouldReturn` full0+  -- the index of outputs by outpoint matches the outputs+  nu <- redisRun env (Redis.zcard (xpubKey env x "utxo"))+  no <- redisRun env (Redis.hlen (xpubKey env x "uop"))+  no `shouldBe` nu+  -- a balance that Redis has wrong, at an address the next change does not+  -- touch, as if a change had been missed+  Just blob <- redisRun env (Redis.get (xpubKey env x "bal"))+  bals <- either fail return (decode blob :: Either String [XPubBal])+  let target = addrOf env x [0, 0]+      wrong :: XPubBal -> XPubBal+      wrong b+        | b.balance.address == target =+            XPubBal {path = b.path, balance = (b.balance :: Balance) {received = b.balance.received + 1}}+        | otherwise = b+  void . redisRun env $ Redis.set (xpubKey env x "bal") (encode (map wrong bals))+  void $ pay env x ref [([0, 3], 500000)]+  waitSynced env+  checkXPub env x ref+  full1 <- meta "full"+  (full1 > full0) `shouldBe` True+  -- the wrong balance is gone: Redis has the balances of the database+  fromDB <- L.withView env.store.db (`L.xPubBals` x.spec)+  Just blob' <- redisRun env (Redis.get (xpubKey env x "bal"))+  decode blob' `shouldBe` Right fromDB++-- | Under load, the database can have transactions whose events the cache has+-- not applied yet, and the balances that a reload reads include them. The+-- reload finds them from the addresses whose balances moved, without reading+-- the xpub whole. Here the entry is wound back to before a payment, as if+-- its event had not come, and a reload is told only of an older change.+catchUpTest :: Env -> IO ()+catchUpTest env = do+  let x = newXPub env "catch-up" DeriveNormal+      key = xpubKey env x+      meta k = redisRun env (Redis.hget (key "meta") k)+  ref <- newLedger+  old <- pay env x ref [([0, 0], 100000)]+  void $ mine env 1+  checkXPub env x ref+  full0 <- meta "full"+  Just blob0 <- redisRun env (Redis.get (key "bal"))+  -- a payment in the mempool, applied by the cache+  newT <- pay env x ref [([0, 1], 200000)]+  waitSynced env+  checkXPub env x ref+  let new = fromMaybe (error "bad txid") (hexToTxHash newT)+      oldH = fromMaybe (error "bad txid") (hexToTxHash old)+  -- wind the entry back: balances, transaction and outputs of the payment+  uop <- redisRun env (Redis.hgetall (key "uop"))+  let paidBy o = either (const False) (\p -> p.hash == new) (decode o :: Either String OutPoint)+      ours = [(op, u) | (op, u) <- uop, paidBy op]+  outs <- maybe (fail "no outputs of the payment in the entry") return (NonEmpty.nonEmpty ours)+  void . redisRun env $ Redis.set (key "bal") blob0+  void . redisRun env $ Redis.zrem (key "txs") (NonEmpty.singleton (encode new))+  void . redisRun env $ Redis.zrem (key "utxo") (snd <$> outs)+  void . redisRun env $ Redis.hdel (key "uop") (fst <$> outs)+  cacheRefresh env.redisCache x.spec (HashSet.singleton oldH) `shouldReturn` Reloaded+  meta "full" `shouldReturn` full0+  redisRun env (Redis.zscore (key "txs") (encode new)) >>= (`shouldSatisfy` isJust)+  checkXPub env x ref+  fromDB <- L.withView env.store.db (`L.xPubBals` x.spec)+  Just blob1 <- redisRun env (Redis.get (key "bal"))+  decode blob1 `shouldBe` Right fromDB++-- | An output of a cached transaction that pays an address outside the xpub+-- is spent by a transaction that does not touch the xpub. The xpub is not+-- reloaded, but the cached body must show the spender.+spenderTest :: Env -> IO ()+spenderTest env = do+  let x = newXPub env "spender" DeriveNormal+  ref <- newLedger+  w <- getNewAddress env.node+  raw <-+    rpc+      env.node+      "createrawtransaction"+      [ toJSON ([] :: [Value]),+        object [Key.fromText (at env x [0, 0]) .= btc 200000, Key.fromText w .= btc 300000]+      ]+  funded <- rpc env.node "fundrawtransaction" [raw]+  signed <- rpc env.node "signrawtransactionwithwallet" [(funded ! "hex")]+  txT <-+    rpc env.node "sendrawtransaction" [(signed ! "hex")] >>= \case+      String t -> return t+      v -> fail (show v)+  record env x ref txT+  void $ mine env 1+  checkXPub env x ref+  info <- rpc env.node "getaddressinfo" [String w]+  tx <- getRawTx env.node txT+  let wScript = textField "scriptPubKey" info+  vout <- case [ numV n+               | o <- arr ((tx ! "vout")),+                 Just n <- [field "n" o],+                 (textField "hex" =<< field "scriptPubKey" o) == wScript+               ] of+    v : _ -> return v+    [] -> fail "No output to the wallet address"+  w2 <- getNewAddress env.node+  raw2 <-+    rpc+      env.node+      "createrawtransaction"+      [ toJSON [object ["txid" .= txT, "vout" .= vout]],+        object [Key.fromText w2 .= btc 290000]+      ]+  signed2 <- rpc env.node "signrawtransactionwithwallet" [raw2]+  txU <-+    rpc env.node "sendrawtransaction" [(signed2 ! "hex")] >>= \case+      String t -> return t+      v -> fail (show v)+  waitSynced env+  let th = fromMaybe (error "bad txid") (hexToTxHash txT)+      spenderOf = \case+        [Just td] -> (.txid) <$> IntMap.lookup (fromIntegral vout) td.spenders+        _ -> Nothing+  waitFor "the spender of a cached body" $+    (== hexToTxHash txU) . spenderOf <$> cacheTxData env.redisCache [th]+  sameAnswers env ["/xpub/" <> x.text <> "/transactions/full", "/transaction/" <> cs txT]+  void $ mine env 1+  sameAnswers env ["/xpub/" <> x.text <> "/transactions/full", "/transaction/" <> cs txT]++-- | Evicting down to one xpub keeps the one used last, and removes everything+-- of the others: their entries, their addresses in the index, and the+-- bodies that no other cached xpub refers to.+evictTest :: Env -> IO ()+evictTest env = do+  let xa = newXPub env "evict a" DeriveNormal+      xb = newXPub env "evict b" DeriveNormal+  ra <- newLedger+  rb <- newLedger+  void $ pay env xa ra [([0, 0], 100000)]+  void $ pay env xb rb [([0, 0], 100000), ([0, 1], 100000)]+  void $ mine env 1+  checkXPub env xb rb+  checkXPub env xa ra+  _ <- getJSON env.cached ("/xpub/" <> xa.text <> "/balances")+  n <- cacheEvict env.redisCache 1+  n `shouldSatisfy` (>= 1)+  isCached env xa `shouldReturn` True+  isCached env xb `shouldReturn` False+  left <- redisRun env $ Redis.keys (xpubKey env xb "*")+  left `shouldBe` []+  addrs <- redisRun env $ Redis.smembers (cachePrefix env.net <> "a:" <> encode (addrOf env xb [0, 0]))+  addrs `shouldBe` []+  eb <- expect <$> readIORef rb+  forM_ eb.txids $ \t -> do+    let h = fromMaybe (error "bad txid") (hexToTxHash t)+    stored <- redisRun env $ Redis.exists (cachePrefix env.net <> "tx:" <> encode h)+    stored `shouldBe` False+  -- the evicted xpub is answered from the database and cached again+  checkXPub env xb rb++-- | The keys of older caches, written before the store started, are as they+-- were.+oldKeysTest :: Env -> IO ()+oldKeysTest env =+  forM_ oldKeys $ \(k, v) ->+    redisRun env (Redis.get k) `shouldReturn` Just v++-- | While Redis does not answer, the store answers from its own database,+-- and its health check says that it does with status 203: not a failure,+-- since the store works, and not the 200 of a store that has everything.+-- When Redis is back, so is the 200. Redis is told to hold every client for+-- a while, which is what a server that hangs looks like from outside.+redisDownTest :: Env -> IO ()+redisDownTest env = do+  let x = newXPub env "redisdown" DeriveNormal+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000), ([1, 0], 50000)]+  void $ mine env 1+  checkXPub env x ref+  let redisOK (_, v) = (v ! "redis") ! "ok"+  (code0, healthy) <- getJSON env.cached "/health"+  code0 `shouldBe` 200+  redisOK (code0, healthy) `shouldBe` Bool True+  -- a store without a cache has no such check+  (_, bare) <- getJSON env.plain "/health"+  bare `shouldSatisfy` \case+    Object o -> not (KeyMap.member "redis" o)+    _ -> False+  held <- redisRun env (Redis.sendRequest ["CLIENT", "PAUSE", "12000"])+  held `shouldBe` Redis.Ok+  -- the first request waits two seconds for Redis and gives up on it+  (code, _) <- getJSON env.cached ("/xpub/" <> x.text <> "/balances")+  code `shouldBe` 200+  waitFor "the health check to notice that Redis is gone" $+    (== 203) . fst <$> getJSON env.cached "/health"+  redisOK <$> getJSON env.cached "/health" `shouldReturn` Bool False+  checkXPub env x ref+  waitFor "the health check to notice that Redis is back" $+    (== 200) . fst <$> getJSON env.cached "/health"+  checkXPub env x ref++-- | An error is answered in binary to a request that asks for binary, and+-- in JSON otherwise, and every answer says that it depends on what was+-- asked for.+errorFormatTest :: Env -> IO ()+errorFormatTest env = do+  let unknown = "/transaction/" <> replicate 64 'a'+      ask accept = do+        let opts =+              Wreq.defaults+                & Wreq.checkResponse ?~ (\_ _ -> return ())+                & maybe id (\a -> Wreq.header "Accept" .~ [a]) accept+        r <- Wreq.getWith opts (env.plain <> unknown)+        return+          ( r ^. Wreq.responseStatus . Wreq.statusCode,+            r ^. Wreq.responseHeader "Content-Type",+            r ^. Wreq.responseHeader "Vary",+            r ^. Wreq.responseBody+          )+  (code, kind, vary, bytes) <- ask (Just "application/octet-stream")+  (code, kind, vary) `shouldBe` (404, "application/octet-stream", "Accept, Accept-Encoding")+  -- the error, as haskoin-store-data encodes it+  decode (BL.toStrict bytes) `shouldBe` Right ThingNotFound+  -- in a list with a weight, which used to give JSON+  (_, listed, _, _) <- ask (Just "text/plain;q=0.1, application/octet-stream;q=0.9")+  listed `shouldBe` "application/octet-stream"+  (code', kind', vary', json) <- ask Nothing+  (code', kind', vary') `shouldBe` (404, "application/json", "Accept, Accept-Encoding")+  (body json ! "error") `shouldBe` String "not-found-or-invalid-arg"++-- | A web server on the store of the tests, given a certificate that is+-- made for the occasion: it answers over HTTPS, and does not answer a+-- request that is not encrypted. The requests are made with curl, told+-- not to mind who signed the certificate.+tlsTest :: Env -> IO ()+tlsTest env = withSystemTempDirectory "haskoin-store-tls-" $ \dir -> withTestLogger $ \logger -> do+  let certificate = dir <> "/cert.pem"+      key = dir <> "/key.pem"+      subject = ["-subj", "/CN=localhost", "-days", "1"]+      files = ["-keyout", key, "-out", certificate]+  (made, _, why) <-+    readProcessWithExitCode "openssl" (["req", "-x509", "-newkey", "rsa:2048", "-nodes"] <> files <> subject) ""+  (made, why) `shouldSatisfy` ((== ExitSuccess) . fst)+  listening $ \(sock, port) -> do+    let config = (webConfig logger env.store) {tls = Just (certificate, key)} :: WebConfig+        status scheme = do+          let url = scheme <> "://127.0.0.1:" <> show port <> "/health"+          let quietly = ["-sk", "-m", "5", "-o", "/dev/null", "-w", "%{http_code}"]+          (_, code, _) <- readProcessWithExitCode "curl" (quietly <> [url]) ""+          return code+    withAsync (runWebSocket config sock) $ \web -> do+      link web+      waitFor "the server to answer over HTTPS" ((== "200") <$> status "https")+      status "http" `shouldNotReturn` "200"++-- | The parts of HTTP that are not about any one endpoint: a request for+-- the headers alone, the question that a browser asks before a request+-- from another site, compression, a list of addresses in the body of a+-- request, and the reference of the API at the root of the server.+httpTest :: Env -> IO ()+httpTest env = do+  let x = newXPub env "http" DeriveNormal+      addresses = T.intercalate "," [at env x [0, i] | i <- [0 .. 2]]+      quiet = Wreq.defaults & Wreq.checkResponse ?~ (\_ _ -> return ())+      statusOf r = r ^. Wreq.responseStatus . Wreq.statusCode+  -- the headers of an answer, without the answer+  headed <- Wreq.headWith quiet (env.plain <> "/block/best")+  (statusOf headed, headed ^. Wreq.responseHeader "Content-Type") `shouldBe` (200, "application/json")+  -- what may be asked from another site+  asked <- Wreq.optionsWith quiet (env.plain <> "/transactions")+  statusOf asked `shouldBe` 204+  asked ^. Wreq.responseHeader "Access-Control-Allow-Methods" `shouldBe` "GET, HEAD, POST, DELETE, OPTIONS"+  asked ^. Wreq.responseHeader "Access-Control-Allow-Origin" `shouldBe` "*"+  -- compressed for a client that reads it, and the same answer+  let balances = "/address/balances?addresses=" <> T.unpack addresses+  packed <- Wreq.getWith (quiet & Wreq.header "Accept-Encoding" .~ ["gzip"]) (env.plain <> balances)+  packed ^. Wreq.responseHeader "Content-Encoding" `shouldBe` "gzip"+  (_, inQuery) <- getJSON env.plain balances+  -- the same list in the body of a request, where it may be as long as it likes+  (posted, inBody) <- postForm env.plain "/address/balances" [("addresses", addresses)]+  posted `shouldBe` 200+  inBody `shouldBe` inQuery+  length (arr inBody) `shouldBe` 3+  -- the reference of the API, as a page and as what the page shows+  page <- Wreq.getWith quiet (env.plain <> "/")+  (statusOf page, page ^. Wreq.responseHeader "Content-Type") `shouldBe` (200, "text/html; charset=utf-8")+  BL.toStrict (page ^. Wreq.responseBody) `shouldSatisfy` C8.isInfixOf "swagger-ui"+  reference <- Wreq.getWith quiet (env.plain <> "/swagger.yaml")+  statusOf reference `shouldBe` 200+  BL.toStrict (reference ^. Wreq.responseBody) `shouldSatisfy` C8.isInfixOf "description: This server"++-- | The events of the server over a WebSocket. A client that says nothing+-- hears of every transaction; one that names an extended public key, an+-- address or a Bloom filter hears of the transactions that touch them, in+-- JSON or in binary as it spoke, and all hear of the block. The key is+-- followed past the gap it had when it was named. A client that comes+-- back with the hash of an event hears what came after it, and one that+-- sends what the server cannot read is disconnected.+eventsTest :: Env -> IO ()+eventsTest env = do+  let x = newXPub env "events" DeriveNormal+      y = newXPub env "events-other" DeriveNormal+      port = read (reverse (takeWhile (/= ':') (reverse env.plain))) :: Int+      client path = WS.runClient "127.0.0.1" port path+      json = BL.toStrict . marshalJSON (env.net, env.ctx)+      binary = marshal (env.net, env.ctx)+      -- every event that a client hears after saying something, in order+      hearing path say heard = client path $ \c -> do+        mapM_ (either (WS.sendTextData c) (WS.sendBinaryData c)) (say :: [Either C8.ByteString C8.ByteString])+        forever $+          WS.receiveDataMessage c >>= \case+            WS.Text bytes _ -> modifyIORef' heard (either error id (eitherDecode bytes) :)+            WS.Binary bytes -> modifyIORef' heard (either error id (decode (BL.toStrict bytes)) :)+      eventId = \case+        EventTx h -> txHashToHex h+        EventBlock h -> blockHashToHex h+      element = \case+        PubKeyAddress h -> runPutS (serialize h)+        _ -> error "Not the hash of a key"+      bloom = bloomInsert (bloomCreate 10 0.0000001 0 BloomUpdateNone) (element (addrOf env y [0, 1]))+  [all', key, address, filtered, quiet, keyFirst, addressFirst] <- mapM (const (newIORef [])) [1 .. 7 :: Int]+  listeners <-+    mapM+      async+      [ hearing "/events" [] all',+        hearing "/events" [Left (json (EventWatchXPub x.spec))] key,+        hearing "/events" [Right (binary (EventWatch [addrOf env y [0, 0]]))] address,+        hearing "/events" [Right (binary (EventFilterLoad bloom))] filtered,+        -- chosen in the URL, before the first event: blocks alone, a key+        -- under a type of address, an address+        hearing "/events?quiet=true" [] quiet,+        hearing ("/events?derive=legacy&xpub=" <> x.text) [] keyFirst,+        hearing ("/events?addresses=" <> T.unpack (at env y [0, 0])) [] addressFirst+      ]+  mapM_ link listeners+  threadDelay 1000000+  ref <- newLedger+  other <- newLedger+  let told heard n = waitFor "a client to hear of the events" ((>= n) . length <$> readIORef heard)+      paid who ledger path = do+        txid <- pay env who ledger [(path, 100000)]+        -- one at a time, so that the order of the events is known+        told all' . (+ 1) . length =<< readIORef all'+        maybe (fail "Not a hash") (return . EventTx) (hexToTxHash txid)+  one <- paid x ref [0, 0]+  two <- paid y other [0, 0]+  three <- paid y other [0, 1]+  -- past the gap of the key as it was named, within it since its payment+  four <- paid x ref [0, gap]+  [mined] <- mine env 1+  block <- maybe (fail "Not a hash") (return . EventBlock) (hexToBlockHash mined)+  told all' 5+  told key 3+  told address 2+  told filtered 2+  told quiet 1+  told keyFirst 3+  told addressFirst 2+  threadDelay 500000+  mapM_ cancel listeners+  (reverse <$> readIORef all') `shouldReturn` [one, two, three, four, block]+  (reverse <$> readIORef key) `shouldReturn` [one, four, block]+  (reverse <$> readIORef address) `shouldReturn` [two, block]+  (reverse <$> readIORef filtered) `shouldReturn` [three, block]+  (reverse <$> readIORef quiet) `shouldReturn` [block]+  (reverse <$> readIORef keyFirst) `shouldReturn` [one, four, block]+  (reverse <$> readIORef addressFirst) `shouldReturn` [two, block]+  -- from an event on: what came after it+  let since = "/events?after=" <> T.unpack (eventId one)+  again <- newIORef []+  resumed <- async (hearing since [] again)+  told again 4+  cancel resumed+  (reverse <$> readIORef again) `shouldReturn` [two, three, four, block]+  -- and the same without a WebSocket, one event to a line+  streamed <- eventLines (env.plain <> since) 4+  map (either error id . eitherDecode . BL.fromStrict) streamed `shouldBe` [two, three, four, block]+  -- the stream too can be of what touches an address+  chosen <- eventLines (env.plain <> since <> "&addresses=" <> T.unpack (at env y [0, 1])) 2+  map (either error id . eitherDecode . BL.fromStrict) chosen `shouldBe` [three, block]+  fst <$> getJSON env.plain "/events?addresses=nonsense" `shouldReturn` 400+  fst <$> getJSON env.plain "/events?derive=segwit" `shouldReturn` 400+  -- an event that the server does not have, and a hash that is not one+  fst <$> getJSON env.plain ("/events?after=" <> replicate 64 '0') `shouldReturn` 404+  fst <$> getJSON env.plain "/events?after=nonsense" `shouldReturn` 400+  refused <- tryAny (client ("/events?after=" <> replicate 64 '0') (\_ -> return ()))+  either (const True) (const False) refused `shouldBe` True+  -- a command that cannot be read ends the connection, with the reason+  closed <- try . client "/events" $ \c -> do+    WS.sendTextData c ("{\"type\":\"nonsense\"}" :: C8.ByteString)+    void (WS.receiveDataMessage c)+  case closed of+    Left (WS.CloseRequest code why) -> (code, why) `shouldBe` (1008, "Could not read the command")+    wrong -> expectationFailure ("Not closed: " <> show wrong)++-- | The first lines of a stream of events, which has no end: the request+-- is given up once they are there.+eventLines :: String -> Int -> IO [C8.ByteString]+eventLines url n = do+  got <- newIORef C8.empty+  let collect () chunk = do+        modifyIORef' got (<> chunk)+        enough <- (>= n) . C8.count '\n' <$> readIORef got+        when enough (throwIO (userError "enough"))+  _ <- tryAny (Wreq.foldGet collect () url)+  take n . C8.lines <$> readIORef got++-- | Two more stores, each with its own database, syncing from the same+-- node, as members of one fleet on the Redis of the tests, each with a web+-- server. The fleet agrees on the best block; a payment that both stores+-- hear of from the node reaches the pool once, and both answer every+-- question about it alike, with the one time that the pool has for it; and+-- when a block confirms it, the fleet moves there and the pool lets it go.+--+-- The payment is to an extended public key, which the fleet's cache of+-- them takes after the first request about it. Each store must then+-- answer about the key from the cache as it does when asked to answer from+-- its database, with the payment in the pool and after the block: the+-- cache is brought to the block by whichever store moved the tip.+fleetTest :: Env -> IO ()+fleetTest env = withTestLogger $ \logger -> member logger $ \a urlA -> member logger $ \b urlB -> do+  one <- maybe (fail "The store is not a member of a fleet") return a.fleet+  let x = newXPub env "fleet" DeriveNormal+      q = "?derive=" <> deriveParam x+      address = "/address/" <> T.unpack (at env x [0, 0])+      xpub = "/xpub/" <> x.text+      paths txid =+        [ address <> "/balance",+          address <> "/transactions/full",+          address <> "/unspent",+          xpub <> q,+          xpub <> "/transactions/full" <> q,+          xpub <> "/unspent" <> q,+          "/mempool",+          "/transaction/" <> T.unpack txid+        ]+      atTip = waitFor "the fleet tip to be the best block of the node" $ do+        best <- bestBlockHash env.node+        tip <- memberTip one+        return (fmap blockHashToHex (tip >>= either (const Nothing) Just . decode) == Just best)+      alike what ps = waitFor what $ do+        fromA <- mapM (getJSON urlA) ps+        fromB <- mapM (getJSON urlB) ps+        return (fromA == fromB && all ((== 200) . fst) fromA)+      inPool txid = do+        let h = fromMaybe (error "bad txid") (hexToTxHash txid)+        redisRun env (Redis.hexists (cachePrefix env.net <> "pool:spends") (encode h))+      listed url = length . arr . snd <$> getJSON url (address <> "/transactions")+      about =+        [ xpub <> q,+          xpub <> "/balances" <> q,+          xpub <> "/transactions" <> q,+          xpub <> "/transactions/full" <> q,+          xpub <> "/unspent" <> q+        ]+      cachedKey = cachePrefix env.net <> "fx:e:" <> xpubId x.spec <> ":meta"+      -- the cache has the key, at the fleet tip, and each store answers+      -- about it as it does without the cache, and as the other store does+      throughCache what = do+        waitFor ("the cache to have the key, " <> what) $ do+          _ <- getJSON urlA (xpub <> "/balances" <> q)+          has <- redisRun env (Redis.exists cachedKey)+          tip <- memberTip one+          moves <- redisRun env (Redis.get (cachePrefix env.net <> "fleet:moves"))+          cached <- redisRun env (Redis.get (cachePrefix env.net <> "fx:tip"))+          let here = (\n t -> n <> "@" <> t) <$> moves <*> tip+          return (has && isJust tip && cached == here)+        alike ("the two stores to answer alike from the cache, " <> what) about+        waitFor ("each store to answer from the cache as from its database, " <> what) $ do+          let direct = map (<> "&nocache=true") about+          cachedA <- mapM (getJSON urlA) about+          directA <- mapM (getJSON urlA) direct+          cachedB <- mapM (getJSON urlB) about+          directB <- mapM (getJSON urlB) direct+          return (cachedA == directA && cachedB == directB && cachedA == cachedB)+  waitStore env a+  waitStore env b+  atTip+  -- what each store tells its clients from here on+  heardA <- newIORef []+  heardB <- newIORef []+  let said = \case+        StoreBestBlock h -> Just ("block " <> blockHashToHex h)+        StoreMempoolNew t -> Just ("new " <> txHashToHex t)+        StoreMempoolDelete t -> Just ("gone " <> txHashToHex t)+        _ -> Nothing+      listen st heard = withSubscription st.events $ \sub ->+        forever (receive sub >>= \e -> forM_ (said e) (\line -> modifyIORef' heard (line :)))+  listenA <- async (listen a heardA)+  listenB <- async (listen b heardB)+  threadDelay 500000+  ref <- newLedger+  txid <- pay env x ref [([0, 0], 100000)]+  waitFor "the pool of the fleet to have the payment" (inPool txid)+  waitFor "both stores to list the payment" $ (== (1, 1)) <$> ((,) <$> listed urlA <*> listed urlB)+  alike "the two stores to answer alike, with the payment in the pool" (paths txid)+  throughCache "with the payment in the pool"+  -- the payment is unconfirmed in the answers, at the time that the pool has+  (_, unconfirmed) <- getJSON urlA ("/transaction/" <> T.unpack txid)+  (unconfirmed ! "block") `shouldSatisfy` \case+    Object o -> KeyMap.member "mempool" o+    _ -> False+  void (mine env 1)+  waitStore env a+  waitStore env b+  atTip+  inPool txid `shouldReturn` False+  alike "the two stores to answer alike, with the payment confirmed" (paths txid)+  throughCache "with the payment confirmed"+  -- both told the same events in the same order: the payment as it+  -- entered the pool, and then the block, which is not told as the+  -- payment leaving+  best <- bestBlockHash env.node+  waitFor "both stores to tell of the block" $ do+    told <- (,) <$> readIORef heardA <*> readIORef heardB+    return (("block " <> best) `elem` fst told && ("block " <> best) `elem` snd told)+  toldA <- reverse <$> readIORef heardA+  toldB <- reverse <$> readIORef heardB+  toldA `shouldBe` toldB+  toldA `shouldBe` ["new " <> txid, "block " <> best]+  cancel listenA+  cancel listenB+  -- and a client that heard of the payment from one takes up on the other+  let since = "/events?after=" <> T.unpack txid+      then' = "{\"type\":\"block\",\"id\":\"" <> cs best <> "\"}"+  eventLines (urlA <> since) 1 `shouldReturn` [then']+  eventLines (urlB <> since) 1 `shouldReturn` [then']+  (_, confirmed) <- getJSON urlB ("/transaction/" <> T.unpack txid)+  (confirmed ! "block") `shouldSatisfy` \case+    Object o -> KeyMap.member "height" o+    _ -> False+  -- the health check runs every few seconds, and first ran before the+  -- store had synced+  waitFor "the health check of a member to pass" ((== 200) . fst <$> getJSON urlA "/health")+  (_, health) <- getJSON urlA "/health"+  ((health ! "redis") ! "ok") `shouldBe` Bool True+  where+    member logger f = withRamDir $ \dir ->+      withStore (env.memberIn dir) $ \s ->+        listening $ \(sock, port) ->+          withAsync (runWebSocket (webConfig logger s) sock) $ \web ->+            link web >> f s ("http://127.0.0.1:" <> show port)++-- | Run a second store, with its own database, syncing from the same node+-- and sharing the same Redis, while an action runs.+withOther :: Env -> (Store -> IO a) -> IO a+withOther env f = withRamDir $ \dir -> withStore (env.configIn dir) f++-- | Wait until a store has the best block of the node.+waitStore :: Env -> Store -> IO ()+waitStore env st =+  waitFor "the second store to sync" $ do+    best <- bestBlockHash env.node+    ours <- Q.getBestBlock st.db+    return (fmap blockHashToHex ours == Just best)++-- | Two stores share a Redis and see the same events. A payment to a cached+-- xpub, then the block that confirms it, are each applied once: one store+-- claims each change and reloads the xpub, the other finds it done.+--+-- The stores record their own times for mempool transactions, so which of+-- them wrote the entry shows in the times of its transactions; only the+-- balances are compared with the database here.+onceTest :: Env -> IO ()+onceTest env = withOther env $ \other -> do+  cache2 <- maybe (fail "The second store has no cache") return other.cache+  let x = newXPub env "once" DeriveNormal+      both = do+        a <- cacheCounters env.redisCache+        b <- cacheCounters cache2+        return (a.reloads + b.reloads, a.behind + b.behind)+      synced = waitSynced env >> waitStore env other+      balancesMatch =+        waitFor "the cached balances" $ do+          cached <- cacheXPubBals env.redisCache x.spec+          stored <- L.withView env.store.db (`L.xPubBals` x.spec)+          return (cached == Just stored)+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000)]+  void $ mine env 1+  synced+  _ <- getJSON env.cached ("/xpub/" <> x.text <> "/balances")+  waitCached env x+  threadDelay 1000000+  (r0, b0) <- both+  void $ pay env x ref [([0, 1], 200000)]+  synced+  balancesMatch+  void $ mine env 1+  synced+  balancesMatch+  threadDelay 2000000+  (r1, b1) <- both+  (r1 - r0, b1 - b0) `shouldBe` (2, 0)++-- | Redis has a transaction of a cached xpub that this store has never seen:+-- a store that is behind must not replace the entry, which would drop it.+unknownTest :: Env -> IO ()+unknownTest env = do+  let x = newXPub env "unknown" DeriveNormal+      unknown = encode (TxHash (sha256 ("never seen" :: C8.ByteString)))+  ref <- newLedger+  void $ pay env x ref [([0, 0], 100000)]+  void $ mine env 1+  checkXPub env x ref+  void . redisRun env $ Redis.zadd (xpubKey env x "txs") [(1, unknown)]+  cacheRefresh env.redisCache x.spec HashSet.empty `shouldReturn` Behind+  void . redisRun env $ Redis.zrem (xpubKey env x "txs") (pure unknown)+  cacheRefresh env.redisCache x.spec HashSet.empty `shouldReturn` Reloaded+  checkXPub env x ref
+ test/Haskoin/Store/Logic/LayeredSpec.hs view
@@ -0,0 +1,264 @@+module Haskoin.Store.Logic.LayeredSpec (spec) where++import Control.Monad (foldM, forM_, void)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Either (isLeft)+import Data.List (sortOn)+import Data.Maybe (isJust)+import Data.Serialize (decode)+import Haskoin+  ( Block (..),+    BlockHash,+    Ctx,+    Network,+    OutPoint (..),+    Tx (..),+    TxHash,+    TxIn (..),+    TxOut (..),+    bchRegTest,+    btcRegTest,+    headerHash,+    prepareContext,+    txHash,+  )+import Haskoin.Store.Common (Start (..), StoreRead (..))+import Haskoin.Store.Data (TxData (..), Unspent (..))+import Haskoin.Store.Database.Mempool qualified as M+import Haskoin.Store.Database.Migrate (upgradeDatabase)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Query.Layered qualified as L+import Haskoin.Store.Database.Reader+  ( DatabaseReader (..),+    blockCF,+    heightCF,+    mempoolAddrCF,+    mempoolCF,+    txCF,+  )+import Haskoin.Store.Histories+import Haskoin.Store.Logic (ImportException (..))+import Haskoin.Store.TestBlocks (coinbaseTx, script)+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck (choose, forAll, ioProperty, property, tabulate, vectorOf)+import Test.QuickCheck.Gen (unGen)+import Test.QuickCheck.Random (mkQCGen)+import UnliftIO (withSystemTempDirectory)++spec :: Spec+spec = prepareContext $ \ctx -> describe "rules of the layered layout" $ do+  forM_ [("Bitcoin", btcRegTest), ("Bitcoin Cash, without replace-by-fee", bchRegTest)] $ \(name, net) ->+    modifyMaxSuccess (const 40) $+      it ("leave the database that today's rules and the migration leave, on " <> name) $+        -- the number says when the third store is upgraded, and which+        -- pages are asked for at the end+        property . forAll ((,) <$> vectorOf 70 anyNumber <*> choose (0, 1000000)) $ \(seeds, number) -> ioProperty $ do+          (outcomes, hist) <- sameHistory ctx net number [\db h -> plan db h seed | seed <- seeds]+          let count p = length (filter p outcomes)+              bucket n+                | n == 0 = "none"+                | n <= 3 = "1 to 3"+                | n <= 9 = "4 to 9"+                | otherwise = "10 or more" :: String+          return+            . tabulate "operations refused" [bucket (count (isLeft . (.result)))]+            . tabulate "operations that removed from the mempool" [bucket (count (not . null . (.removed)))]+            $ tabulate "blocks at the end" [bucket (length hist.chain)] True++  it "give a transaction that is confirmed twice the place of the later block, as today's" $ do+    -- the coinbase of the first block again, in the second; then its output+    -- is spent, the spend confirmed, and both blocks reverted+    let twice = coinbaseTx 1+        spend = Tx 2 [TxIn (OutPoint (txHash twice) 0) "" maxBound] [TxOut 4000 (p2pkh 1)] [] 0+        mine txs db h = (h,) . Just <$> blockOn db txs+        pay tx _ h = return (h, Just (Pay tx h.clock))+        revert _ h = return $ case h.chain of+          (b, _) : _ -> (h, Just (Revert (headerHash b.header)))+          [] -> (h, Nothing)+    -- the third store is upgraded with the spend in its mempool+    (outcomes, _) <- sameHistory ctx btcRegTest 2 [mine [twice], pay spend, mine [spend], revert, revert]+    map (either show (const "done") . (.result)) outcomes `shouldBe` replicate 5 "done"++  it "refuse a block that spends an output of the mempool, which today's rules import" $+    withSystemTempDirectory "haskoin-store-layered-" $ \dir ->+      openLayered btcRegTest ctx dir $ \db -> do+        hist <- start layeredRules db+        let parent = Tx 2 [TxIn (coinOf hist) "" maxBound] [TxOut 4000 script] [] 0+            child = Tx 2 [TxIn (OutPoint (txHash parent) 0) "" maxBound] [TxOut 3000 (p2pkh 1)] [] 0+        (.result) <$> apply layeredRules db (Pay parent 1000) `shouldReturn` Right "True"+        untouched <- everything db+        -- the parent is not in the block, so its output is not in the chain+        block <- blockOn db [child]+        (.result) <$> apply layeredRules db block `shouldReturn` Left Orphan+        everything db `shouldReturn` untouched++  it "leave the keys of a mempool transaction alone when a block confirms what it spends" $+    withSystemTempDirectory "haskoin-store-layered-" $ \dir ->+      openLayered btcRegTest ctx dir $ \db -> do+        hist <- start layeredRules db+        let parent = Tx 2 [TxIn (coinOf hist) "" maxBound] [TxOut 4000 script, TxOut 5000 (p2pkh 2)] [] 0+            child = Tx 2 [TxIn (OutPoint (txHash parent) 0) "" maxBound] [TxOut 3000 (p2pkh 1)] [] 0+            keysOf tx = do+              Just td <- M.getMempoolTx db (txHash tx)+              let r = M.mempoolRecords db.net db.ctx 1000 td+              return (fst (M.recordEntries r), M.addressArea [r])+        forM_ [parent, child] $ \tx ->+          (.result) <$> apply layeredRules db (Pay tx 1000) `shouldReturn` Right "True"+        (general, byAddress) <- keysOf child+        block <- blockOn db [parent]+        confirmed <- apply layeredRules db block+        confirmed.removed `shouldBe` [txHash parent]+        -- the child's keys are all that is left of the mempool, unchanged+        dump db (mempoolCF db.db) `shouldReturn` sortOn fst general+        dump db (mempoolAddrCF db.db) `shouldReturn` sortOn fst byAddress+        -- and the spent output of the parent, now in the chain, is still+        -- spent to anyone who asks+        L.withView db (\v -> L.getUnspent v (OutPoint (txHash parent) 0)) `shouldReturn` Nothing+        (fmap (.value) <$> Q.getUnspent db (OutPoint (txHash parent) 0)) `shouldReturn` Just 4000++-- | Give the same history to three stores, each with a database of its+-- own:+--+-- * one that runs the rules of today's layout;+--+-- * one that runs the rules of the layered layout;+--+-- * one that is upgraded on the way: it runs today's rules for the first+--   operations, as many as a number says, then its database is migrated,+--   and it runs the rules of the layered layout from there on.+--+-- Each operation is chosen from the first database and the history so far.+-- Every operation must have the same outcome in all of them, and leave the+-- same answers to every query. At the end all are asked for the same pages,+-- chosen from the number. Then the first database is migrated: it must equal+-- the second key for key, and the third in everything but the bodies of+-- deleted transactions, which each set of rules leaves in its own place.+-- Gives the outcomes and the history.+sameHistory ::+  Ctx ->+  Network ->+  Int ->+  [DatabaseReader -> History -> IO (History, Maybe Op)] ->+  IO ([Outcome], History)+sameHistory ctx net number choices =+  withSystemTempDirectory "haskoin-store-mixed-" $ \dirOld ->+    withSystemTempDirectory "haskoin-store-layered-" $ \dirNew ->+      withSystemTempDirectory "haskoin-store-upgraded-" $ \dirUp -> do+        -- the store to upgrade begins with a database of version 20+        open20 net ctx dirUp (\_ -> return ())+        openLayered net ctx dirNew $ \new ->+          openLayered net ctx dirUp $ \up -> do+            (outcomes, hist) <- open20 net ctx dirOld $ \old -> do+              hist0 <- start mixedRules old+              void (start layeredRules new)+              void (start mixedRules up)+              let (earlier, later) = splitAt (number `mod` (length choices + 1)) choices+                  today op _ _ _ = void (apply mixedRules up op)+                  upgraded op was hist answers = do+                    now <- apply layeredRules up op+                    (show op, now) `shouldBe` (show op, was)+                    M.wrongSummaries up `shouldReturn` []+                    layered up (ask hist) `shouldReturn` answers+              half <- foldM (next old new today) ([], hist0) earlier+              void (upgradeDatabase (\_ -> return ()) up)+              done@(_, hist) <- foldM (next old new upgraded) half later+              -- pages, of databases that the new rules built or carried on+              -- with, against today's+              let asked = unGen (questions net ctx hist 60) (mkQCGen number) 30+              was <- mixed old (\queries -> mapM (answer queries) asked)+              -- but for pages that start at a deleted transaction, which+              -- the layered layout does not know+              known <- mapM (startsKnown old) asked+              let both :: [x] -> [x]+                  both xs = [x | (x, True) <- zip xs known]+              forM_ [new, up] $ \db -> do+                now <- layered db (\queries -> mapM (answer queries) asked)+                take 2 (differences (both asked) (both was) (both now)) `shouldBe` []+              -- the queries of a fleet do not know deleted transactions, so+              -- they are not asked for pages that start at one+              let live queries q = case startOf q of+                    Just (Just (AtTx h)) -> isJust <$> queries.getTxData h+                    _ -> return True+              fair <- layered new (\queries -> mapM (live queries) asked)+              let kept :: [x] -> [x]+                  kept xs = [x | (x, True) <- zip xs fair]+              shared <- pooled new (\queries -> mapM (answer queries) (kept asked))+              take 2 (differences (kept asked) (kept was) shared) `shouldBe` []+              return done+            migrated <- openLayered net ctx dirOld $ \old -> upgradeDatabase (\_ -> return ()) old >> everything old+            mine <- everything new+            -- the same, but for the bodies of deleted transactions, which+            -- version 20 left in the chain: the layered rules keep none+            mine `shouldBe` migrated {inChain = [], deleted = []}+            -- the upgraded store has those that it deleted before the+            -- upgrade in the chain, as version 20 left them, even when the+            -- transaction came back later: the queries, compared after+            -- every operation, do not answer with those+            theirs <- everything up+            theirs {deleted = [], inChain = []} `shouldBe` migrated {deleted = [], inChain = []}+            return (reverse outcomes, hist)+  where+    next old new third (outcomes, hist) chosen =+      chosen old hist >>= \case+        (hist', Nothing) -> return (outcomes, hist')+        (hist', Just op) -> do+          was <- apply mixedRules old op+          now <- apply layeredRules new op+          (show op, now) `shouldBe` (show op, was)+          -- each summary is what the entries of its address add up to+          M.wrongSummaries new `shouldReturn` []+          -- and every answer is the same after it+          let hist'' = record old hist' op was+          answers <- mixed old (ask hist'')+          layered new (ask hist'') `shouldReturn` answers+          -- from the queries of a fleet as well, whose pool is here the+          -- store's own mempool+          pooled new (ask hist'') `shouldReturn` answers+          third op was hist'' answers :: IO ()+          return (was : outcomes, hist'')++-- | Everything in a database of the layered layout.+data Everything = Everything+  { best :: !(Maybe BlockHash),+    blocks :: ![(ByteString, ByteString)],+    heights :: ![(ByteString, ByteString)],+    chain :: !Chain,+    mempool :: ![(ByteString, ByteString)],+    mempoolByAddress :: ![(ByteString, ByteString)],+    -- | transactions that left the mempool unconfirmed, wherever their+    -- bodies are kept+    deleted :: ![(TxHash, TxData)],+    -- | those of them that are kept in the chain, as version 20 did+    inChain :: ![TxHash]+  }+  deriving (Eq, Show)++everything :: DatabaseReader -> IO Everything+everything db = do+  best <- Q.getBestBlock db+  blocks <- dump db (blockCF db.db)+  heights <- dump db (heightCF db.db)+  chain <- chainOf db+  area <- dump db (mempoolCF db.db)+  mempoolByAddress <- dump db (mempoolAddrCF db.db)+  bodies <- dump db (txCF db.db) >>= mapM (decoded . snd)+  let (gone, mempool) = span' ((== "\x0e") . B.take 1 . fst) area+      old = [td | td <- bodies, td.deleted]+  new <- mapM (decoded . snd) gone+  return+    Everything+      { best,+        blocks,+        heights,+        chain,+        mempool,+        mempoolByAddress,+        deleted = sortOn fst [(txHash td.tx, td) | td <- old <> new],+        inChain = map (txHash . (.tx)) old+      }+  where+    decoded :: ByteString -> IO TxData+    decoded = either fail return . decode+    span' p xs = (filter p xs, filter (not . p) xs)
test/Haskoin/Store/LogicSpec.hs view
@@ -1,20 +1,17 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}- module Haskoin.Store.LogicSpec (spec) where  import Control.Monad (forM_, void)-import Control.Monad.Reader (ask, liftIO, runReaderT) import Data.ByteString qualified as B+import Data.Functor ((<&>)) import Data.List (sort, sortOn)+import Data.Maybe (fromMaybe) import Data.Ord (Down (..)) import Data.Text qualified as T import Data.Word (Word32, Word64) import Database.RocksDB.Query (insert, insertCF, matchingAsListCF, retrieve) import Haskoin-  ( Block (..),+  ( Address,+    Block (..),     BlockHash,     BlockHeader (..),     BlockNode (..),@@ -23,7 +20,6 @@     OutPoint (..),     Tx (..),     TxHash (..),-    sha256,     TxIn (..),     TxOut (..),     bchRegTest,@@ -32,10 +28,11 @@     headerHash,     prepareContext,     scriptToAddressBS,+    sha256,     txHash,   ) import Haskoin.Node (LogLevel (..), Logger (..))-import Haskoin.Store.Common (Limits (..), StoreReadBase (..), StoreWrite (..), getActiveTxData, getDefaultBalance)+import Haskoin.Store.Common (Limits (..), StoreRead (..)) import Haskoin.Store.Data   ( Balance (..),     BlockData (..),@@ -43,13 +40,21 @@     Spender (..),     TxData (..),     Unspent (..),+    zeroBalance,   )-import Haskoin.Store.Database.Reader (DatabaseReader (..), DatabaseReaderT, getMempoolBefore, memoryReport, mempoolCF, withDatabaseReader)+import Haskoin.Store.Database.Query qualified as Q+import Haskoin.Store.Database.Reader (DatabaseReader (..), memoryReport, mempoolCF, withDatabaseReader) import Haskoin.Store.Database.Types (MemKey (..), MemTimeKey (..), MemTxKey (..), VersionKey (..))-import Haskoin.Store.Database.Writer (MempoolChanges (..), runWriter, runWriterMempool)+import Haskoin.Store.Database.Writer+  ( MempoolChanges (..),+    Writer,+    addToMempool,+    deleteFromMempool,+    runWriter,+    runWriterMempool,+  ) import Haskoin.Store.Logic   ( ImportException (..),-    ImportM,     deleteUnconfirmedTx,     importBlock,     initBest,@@ -68,7 +73,7 @@       withDB btcRegTest ctx $ \db -> do         importing db initBest `shouldReturn` Right ()         importing db initBest `shouldReturn` Right ()-        reading db getBestBlock `shouldReturn` Just genesis+        Q.getBestBlock db `shouldReturn` Just genesis    describe "memoryReport" $     it "lists the memory of every column family" $@@ -81,70 +86,70 @@   describe "data version 18" $     it "moves the mempool into its column family when opened" $       withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do-        let open :: DatabaseReaderT IO () -> IO ()+        let open :: (DatabaseReader -> IO ()) -> IO ()             open = withDatabaseReader btcRegTest ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing             h1 = TxHash (sha256 ("one" :: B.ByteString))             h2 = TxHash (sha256 ("two" :: B.ByteString))             old = [(200, h2), (100, h1)] :: [(Word64, TxHash)]-        open $ ask >>= \r -> liftIO $ do+        open $ \r -> do           insert r.db VersionKey (18 :: Word32)           insert r.db MemKey old-        open $ ask >>= \r -> liftIO $ do-          reading r getMempool `shouldReturn` old-          reading r (getMempoolPage (Limits 1 1 Nothing)) `shouldReturn` drop 1 old+        open $ \r -> do+          Q.getMempool r `shouldReturn` old+          Q.getMempoolPage r (Limits 1 1 Nothing) `shouldReturn` drop 1 old           (retrieve r.db MemKey :: IO (Maybe [(Word64, TxHash)])) `shouldReturn` Nothing           retrieve r.db VersionKey `shouldReturn` Just (20 :: Word32)    describe "data version 19" $     it "indexes the mempool by time when opened" $       withSystemTempDirectory "haskoin-store-migrate-" $ \dir -> do-        let open :: DatabaseReaderT IO () -> IO ()+        let open :: (DatabaseReader -> IO ()) -> IO ()             open = withDatabaseReader btcRegTest ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing             old = sortOn Down (zip [300, 100, 200] (hashes 3))-        open $ ask >>= \r -> liftIO $ do+        open $ \r -> do           insert r.db VersionKey (19 :: Word32)           forM_ old $ \(t, h) -> insertCF r.db (mempoolCF r.db) (MemTxKey h) t           (matchingAsListCF r.db (mempoolCF r.db) MemTimeKeyS :: IO [(MemTimeKey, ())])             `shouldReturn` []-        open $ ask >>= \r -> liftIO $ do-          reading r getMempool `shouldReturn` old+        open $ \r -> do+          Q.getMempool r `shouldReturn` old           retrieve r.db VersionKey `shouldReturn` Just (20 :: Word32)    describe "mempool index" $ do     let entries = zip [5, 3, 5, 1, 4, 3, 2] (hashes 7)         newest = sortOn Down entries-        fill db = importing db (forM_ entries (\(t, h) -> addToMempool h t))+        fill db = importing db (\w -> forM_ entries (\(t, h) -> addToMempool w h t))     it "pages the mempool newest first" $       withDB btcRegTest ctx $ \db -> do         _ <- fill db-        reading db getMempool `shouldReturn` newest+        Q.getMempool db `shouldReturn` newest         forM_ [(0, 0), (3, 0), (3, 2), (2, 5), (4, 7), (0, 3)] $ \(l, o) ->-          reading db (getMempoolPage (Limits l o Nothing))+          Q.getMempoolPage db (Limits l o Nothing)             `shouldReturn` take' l (drop (fromIntegral o) newest)     it "reads the oldest entries before a time" $       withDB btcRegTest ctx $ \db -> do         _ <- fill db         let oldest = reverse newest-        reading db (getMempoolBefore 4 10) `shouldReturn` takeWhile ((< 4) . fst) oldest-        reading db (getMempoolBefore 4 2) `shouldReturn` take 2 oldest-        reading db (getMempoolBefore 1 10) `shouldReturn` []+        Q.getMempoolBefore db 4 10 `shouldReturn` takeWhile ((< 4) . fst) oldest+        Q.getMempoolBefore db 4 2 `shouldReturn` take 2 oldest+        Q.getMempoolBefore db 1 10 `shouldReturn` []     it "reports and indexes what a batch changes" $       withDB btcRegTest ctx $ \db -> do         let hs = hashes 3             (h1, h2, h3) = (hs !! 0, hs !! 1, hs !! 2)-            batch :: ImportM () -> IO MempoolChanges+            batch :: (Writer -> IO ()) -> IO MempoolChanges             batch = fmap snd . runWriterMempool db.net db.ctx db quietLogger-        c <- batch (addToMempool h1 10 >> addToMempool h2 20)+        c <- batch (\w -> addToMempool w h1 10 >> addToMempool w h2 20)         (sort c.added, c.removed) `shouldBe` (sort [h1, h2], [])         -- a new time moves the entry in the index, and is no change-        batch (addToMempool h1 30) `shouldReturn` MempoolChanges [] []-        reading db getMempool `shouldReturn` [(30, h1), (20, h2)]+        batch (\w -> addToMempool w h1 30) `shouldReturn` MempoolChanges [] []+        Q.getMempool db `shouldReturn` [(30, h1), (20, h2)]         -- removing what is not there, or adding and removing in one batch,         -- is no change either-        batch (deleteFromMempool h3) `shouldReturn` MempoolChanges [] []-        batch (addToMempool h3 40 >> deleteFromMempool h3) `shouldReturn` MempoolChanges [] []-        batch (deleteFromMempool h2) `shouldReturn` MempoolChanges [] [h2]-        reading db getMempool `shouldReturn` [(30, h1)]+        batch (\w -> deleteFromMempool w h3) `shouldReturn` MempoolChanges [] []+        batch (\w -> addToMempool w h3 40 >> deleteFromMempool w h3) `shouldReturn` MempoolChanges [] []+        batch (\w -> deleteFromMempool w h2) `shouldReturn` MempoolChanges [] [h2]+        Q.getMempool db `shouldReturn` [(30, h1)]         (matchingAsListCF db.db (mempoolCF db.db) MemTimeKeyS :: IO [(MemTimeKey, ())])           `shouldReturn` [(MemTimeKey 30 h1, ())] @@ -152,10 +157,10 @@     it "imports a transaction and spends its input" $       withCoin btcRegTest ctx $ \db coin -> do         let tx = spend [coin] [1000, 2000]-        importing db (newMempoolTx tx 1700000000) `shouldReturn` Right True-        reading db getMempool `shouldReturn` [(1700000000, txHash tx)]-        reading db (getSpender coin) `shouldReturn` Just (Spender (txHash tx) 0)-        fmap (.value) <$> reading db (getUnspent (out tx 1)) `shouldReturn` Just 2000+        importing db (\w -> newMempoolTx w tx 1700000000) `shouldReturn` Right True+        Q.getMempool db `shouldReturn` [(1700000000, txHash tx)]+        Q.getSpender db coin `shouldReturn` Just (Spender (txHash tx) 0)+        fmap (.value) <$> Q.getUnspent db (out tx 1) `shouldReturn` Just 2000      it "ignores a transaction it already has" $       withCoin btcRegTest ctx $ \db coin -> do@@ -177,7 +182,7 @@       withCoin btcRegTest ctx $ \db coin -> do         let parent = spendRBF [coin] [1000]             child = spend [out parent 0] [900]-        importing db (mapM (`newMempoolTx` 0) [parent, child])+        importing db (\w -> mapM (\t -> newMempoolTx w t 0) [parent, child])           `shouldReturn` Right [True, True]         mapM (rbfFlag db) [parent, child] `shouldReturn` [Just True, Just True] @@ -200,8 +205,15 @@         mapM (addTx db) [original, child, replacement]           `shouldReturn` [Right True, Right True, Right True]         mempool db `shouldReturn` [txHash replacement]-        reading db (getSpender coin) `shouldReturn` Just (Spender (txHash replacement) 0)-        fmap (.deleted) <$> reading db (getTxData (txHash original)) `shouldReturn` Just True+        Q.getSpender db coin `shouldReturn` Just (Spender (txHash replacement) 0)+        fmap (.deleted) <$> Q.getTxData db (txHash original) `shouldReturn` Just True+        -- the database keeps the replaced ones, and the queries that the API+        -- answers with do not return them, by hash or among others+        let queries = Q.databaseRead db+            wanted = map txHash [original, child, replacement]+        queries.getTxData (txHash original) `shouldReturn` Nothing+        map (fmap (txHash . (.tx))) <$> queries.getManyTxData wanted+          `shouldReturn` [Nothing, Nothing, Just (txHash replacement)]      it "replaces a transaction and its descendant when both conflict" $       withCoin btcRegTest ctx $ \db coin1 -> do@@ -223,13 +235,13 @@         addTx db original `shouldReturn` Right True         -- The block store imports mempool transactions in batches and skips         -- the ones that are rejected.-        let tryAdd tx = newMempoolTx tx 0 `catch` skip-            skip :: ImportException -> ImportM Bool+        let tryAdd w tx = newMempoolTx w tx 0 `catch` skip+            skip :: ImportException -> IO Bool             skip _ = return False             unfunded = spend [coin1] [5000000001]             confirmedConflict = spend [coin1, coin2] [2000]             spendsReplaced = spend [coin1, out original 0] [900]-        importing db (mapM tryAdd [unfunded, confirmedConflict, spendsReplaced])+        importing db (\w -> mapM (tryAdd w) [unfunded, confirmedConflict, spendsReplaced])           `shouldReturn` Right [False, False, False]         mempool db `shouldReturn` [txHash original] @@ -249,10 +261,10 @@       withCoin btcRegTest ctx $ \db coin -> do         let good = spend [coin] [1000]             orphan = spend [out (coinbaseTx 9) 0] [1]-        importing db (newMempoolTx good 0 >> newMempoolTx orphan 0)+        importing db (\w -> newMempoolTx w good 0 >> newMempoolTx w orphan 0)           `shouldReturn` Left Orphan         mempool db `shouldReturn` []-        reading db (getSpender coin) `shouldReturn` Nothing+        Q.getSpender db coin `shouldReturn` Nothing    describe "importBlock" $ do     it "confirms mempool transactions" $@@ -262,7 +274,7 @@         _ <- mine db [tx]         mempool db `shouldReturn` []         fmap (\t -> (t.block, t.rbf)) <$> txData db tx `shouldReturn` Just (BlockRef 2 1, False)-        fmap (.block) <$> reading db (getUnspent (out tx 0)) `shouldReturn` Just (BlockRef 2 1)+        fmap (.block) <$> Q.getUnspent db (out tx 0) `shouldReturn` Just (BlockRef 2 1)      it "confirms a transaction whose outputs are spent in the mempool" $       withCoin btcRegTest ctx $ \db coin -> do@@ -272,7 +284,7 @@         _ <- mine db [parent]         mempool db `shouldReturn` [txHash child]         fmap (.block) <$> txData db parent `shouldReturn` Just (BlockRef 2 1)-        reading db (getSpender (out parent 0)) `shouldReturn` Just (Spender (txHash child) 0)+        Q.getSpender db (out parent 0) `shouldReturn` Just (Spender (txHash child) 0)      it "stops flagging a descendant once its signalling ancestor confirms" $       withCoin btcRegTest ctx $ \db coin -> do@@ -287,7 +299,7 @@         let parent = spend [coin] [4000000000]             child = spend [out parent 0] [3999990000]         block <- mine db [child, parent]-        Just bd <- reading db (getBlock (headerHash block.header))+        Just bd <- Q.getBlock db (headerHash block.header)         (bd.height, bd.main, bd.txs) `shouldBe` (2, True, map txHash block.txs)         (bd.subsidy, bd.fee, bd.outputs) `shouldBe` (5000000000, 1000010000, 7999990000)         fmap (.block) <$> txData db child `shouldReturn` Just (BlockRef 2 1)@@ -304,17 +316,17 @@      it "rejects blocks that spend unknown outputs, changing nothing" $       withCoin btcRegTest ctx $ \db _ -> do-        best <- reading db getBestBlock+        best <- Q.getBestBlock db         tryMine db [spend [out (coinbaseTx 9) 0] [1]] `shouldReturn` Left Orphan-        reading db getBestBlock `shouldReturn` best+        Q.getBestBlock db `shouldReturn` best      it "rejects blocks that do not build on the best block" $       withDB btcRegTest ctx $ \db -> do         let (block1, node1) = nextBlock btcRegTest 0 (genesisNode btcRegTest).header []             (block2, node2) = nextBlock btcRegTest 1 node1.header []-        importing db (void (importBlock block1 node1)) `shouldReturn` Left BestBlockNotFound+        importing db (\w -> void (importBlock w block1 node1)) `shouldReturn` Left BestBlockNotFound         importing db initBest `shouldReturn` Right ()-        importing db (void (importBlock block2 node2)) `shouldReturn` Left PrevBlockNotBest+        importing db (\w -> void (importBlock w block2 node2)) `shouldReturn` Left PrevBlockNotBest    describe "revertBlock" $ do     it "returns the block's transactions to the mempool" $@@ -322,10 +334,10 @@         let tx = spendRBF [coin] [1000]         block <- mine db [tx]         let h = headerHash block.header-        importing db (revertBlock h) `shouldReturn` Right ()-        reading db getBestBlock `shouldReturn` Just block.header.prev-        fmap (.main) <$> reading db (getBlock h) `shouldReturn` Just False-        reading db getMempool `shouldReturn` [(1296689802, txHash tx)]+        importing db (`revertBlock` h) `shouldReturn` Right ()+        Q.getBestBlock db `shouldReturn` Just block.header.prev+        fmap (.main) <$> Q.getBlock db h `shouldReturn` Just False+        Q.getMempool db `shouldReturn` [(1296689802, txHash tx)]         rbfFlag db tx `shouldReturn` Just True         txData db (coinbaseTx 2) `shouldReturn` Nothing @@ -336,15 +348,15 @@         block <- mine db [parent]         addTx db child `shouldReturn` Right True         rbfFlag db child `shouldReturn` Just False-        importing db (revertBlock (headerHash block.header)) `shouldReturn` Right ()+        importing db (`revertBlock` (headerHash block.header)) `shouldReturn` Right ()         rbfFlag db child `shouldReturn` Just True      it "only reverts the best block" $       withDB btcRegTest ctx $ \db -> do-        importing db (revertBlock genesis) `shouldReturn` Left BestBlockNotFound+        importing db (`revertBlock` genesis) `shouldReturn` Left BestBlockNotFound         importing db initBest `shouldReturn` Right ()         _ <- mine db []-        importing db (revertBlock genesis) `shouldReturn` Left BlockNotBest+        importing db (`revertBlock` genesis) `shouldReturn` Left BlockNotBest    describe "deleteUnconfirmedTx" $ do     it "deletes a transaction and its descendants" $@@ -352,18 +364,18 @@         let parent = spend [coin] [1000]             child = spend [out parent 0] [900]         mapM (addTx db) [parent, child] `shouldReturn` [Right True, Right True]-        importing db (deleteUnconfirmedTx False (txHash parent)) `shouldReturn` Right ()+        importing db (\w -> deleteUnconfirmedTx w False (txHash parent)) `shouldReturn` Right ()         mempool db `shouldReturn` []-        fmap (.value) <$> reading db (getUnspent coin) `shouldReturn` Just 5000000000+        fmap (.value) <$> Q.getUnspent db coin `shouldReturn` Just 5000000000      it "ignores unknown transactions" $       withCoin btcRegTest ctx $ \db _ ->-        importing db (deleteUnconfirmedTx False (txHash (coinbaseTx 9)))+        importing db (\w -> deleteUnconfirmedTx w False (txHash (coinbaseTx 9)))           `shouldReturn` Right ()      it "refuses to delete confirmed transactions" $       withCoin btcRegTest ctx $ \db _ ->-        importing db (deleteUnconfirmedTx False (txHash (coinbaseTx 1)))+        importing db (\w -> deleteUnconfirmedTx w False (txHash (coinbaseTx 1)))           `shouldReturn` Left TxConfirmed    describe "address balances" $@@ -373,14 +385,14 @@         let tx = spend [coin] [1000, 2000]             -- confirmed, unconfirmed, outputs, transactions, received             balance = do-              b <- reading db (getDefaultBalance a)+              b <- defaultBalance db a               return (b.confirmed, b.unconfirmed, b.utxo, b.txs, b.received)         balance `shouldReturn` (5000000000, 0, 1, 1, 5000000000)         addTx db tx `shouldReturn` Right True         balance `shouldReturn` (0, 3000, 2, 2, 5000003000)         block <- mine db [tx]         balance `shouldReturn` (5000003000, 0, 3, 3, 10000003000)-        importing db (revertBlock (headerHash block.header)) `shouldReturn` Right ()+        importing db (`revertBlock` (headerHash block.header)) `shouldReturn` Right ()         balance `shouldReturn` (0, 3000, 2, 2, 5000003000)    describe "a coin whose script has no address on the network" $@@ -391,14 +403,14 @@         let hashLock = "\xaa\x20" <> B.replicate 32 0xbb <> "\x87"             tx = Tx 2 [TxIn coin "" maxBound] [TxOut 1000 hashLock] [] 0         addTx db tx `shouldReturn` Right True-        coinAddress <- fmap (.address) <$> reading db (getUnspent (out tx 0))+        coinAddress <- fmap (.address) <$> Q.getUnspent db (out tx 0)         coinAddress `shouldBe` Just Nothing  -- | Run with an empty database in a temporary directory. withDB :: Network -> Ctx -> (DatabaseReader -> IO a) -> IO a withDB net ctx go =   withSystemTempDirectory "haskoin-store-logic-" $ \dir ->-    withDatabaseReader net ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing (ask >>= liftIO . go)+    withDatabaseReader net ctx 20 32 dir False Nothing Nothing Nothing Nothing Nothing go  -- | Run with the genesis block and one mined block, passing the output of -- that block's coinbase, worth 5000000000.@@ -409,7 +421,7 @@     _ <- mine db []     go db (out (coinbaseTx 1) 0) -importing :: DatabaseReader -> ImportM a -> IO (Either ImportException a)+importing :: DatabaseReader -> (Writer -> IO a) -> IO (Either ImportException a) importing db = try . runWriter db.net db.ctx db quietLogger  -- | Build every message, so that log lines are tested too, but print none.@@ -430,28 +442,32 @@ take' 0 = id take' l = take (fromIntegral l) -reading :: DatabaseReader -> DatabaseReaderT IO a -> IO a-reading db f = runReaderT f db+-- | Balance of an address, or a zero balance if never used.+defaultBalance :: DatabaseReader -> Address -> IO Balance+defaultBalance db a = fromMaybe (zeroBalance a) <$> Q.getBalance db a  addTx :: DatabaseReader -> Tx -> IO (Either ImportException Bool)-addTx db tx = importing db (newMempoolTx tx 0)+addTx db tx = importing db (\w -> newMempoolTx w tx 0)  txData :: DatabaseReader -> Tx -> IO (Maybe TxData)-txData db = reading db . getActiveTxData . txHash+txData db tx =+  Q.getTxData db (txHash tx) <&> \case+    Just td | not td.deleted -> Just td+    _ -> Nothing  rbfFlag :: DatabaseReader -> Tx -> IO (Maybe Bool) rbfFlag db tx = fmap (.rbf) <$> txData db tx  mempool :: DatabaseReader -> IO [TxHash]-mempool db = sort . map snd <$> reading db getMempool+mempool db = sort . map snd <$> Q.getMempool db  -- | Mine a block with these transactions on top of the best block. tryMine :: DatabaseReader -> [Tx] -> IO (Either ImportException Block) tryMine db txs = do-  Just h <- reading db getBestBlock-  Just parent <- reading db (getBlock h)+  Just h <- Q.getBestBlock db+  Just parent <- Q.getBlock db h   let (block, node) = nextBlock db.net parent.height parent.header txs-  fmap (const block) <$> importing db (importBlock block node)+  fmap (const block) <$> importing db (\w -> importBlock w block node)  mine :: DatabaseReader -> [Tx] -> IO Block mine db txs = tryMine db txs >>= either (fail . ("Block rejected: " <>) . show) return
test/Haskoin/Store/TestBlocks.hs view
@@ -1,7 +1,3 @@-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}- -- | Regtest blocks for tests. Their headers carry valid proof of work, so -- they pass haskoin-node's header checks as well as the store's import. module Haskoin.Store.TestBlocks
+ test/Haskoin/Store/Web/EventsSpec.hs view
@@ -0,0 +1,191 @@+module Haskoin.Store.Web.EventsSpec (spec) where++import Control.Concurrent.STM (atomically, newTVarIO, orElse, readTVarIO, stateTVar)+import Data.ByteString qualified as B+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize)+import Data.Foldable (toList)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.Maybe (fromMaybe)+import Data.Word (Word32, Word8)+import Haskoin+import Haskoin.Store.Common (deriveFunction)+import Haskoin.Store.Data+import Haskoin.Store.Web.Events+import Test.Hspec++-- | An address that nothing else here has.+address :: Word8 -> Address+address n = PubKeyAddress (addressHash (B.replicate 33 n))++-- | A transaction that pays to addresses, told apart by a number.+paying :: Ctx -> Word32 -> [Address] -> Seen+paying ctx n to =+  Seen {tx, addresses = HashSet.fromList to, size = 1000}+  where+    tx = Tx {version = 1, inputs = [], outputs = map out to, witness = [], locktime = n}+    out a = TxOut {value = 1000, script = fromMaybe (error "No script") (addressToScriptBS ctx a)}++-- | A key, and its address at a place on a chain.+key :: Ctx -> DeriveType -> XPubSpec+key ctx deriv = XPubSpec {key = deriveXPubKey ctx (makeXPrvKey (B.replicate 32 7)), deriv}++keyAddress :: Ctx -> XPubSpec -> Word32 -> Word32 -> Address+keyAddress ctx x chain = deriveFunction ctx x.deriv (pubSubKey ctx x.key chain)++-- | A balance of a key's address that has been paid to.+used :: Ctx -> XPubSpec -> Word32 -> Word32 -> XPubBal+used ctx x chain i =+  XPubBal+    { path = [chain, i],+      balance =+        Balance+          { address = keyAddress ctx x chain i,+            confirmed = 1,+            unconfirmed = 0,+            utxo = 1,+            txs = 1,+            received = 1+          }+    }++spec :: Spec+spec = prepareContext $ \ctx -> do+  describe "log of events" $ do+    it "gives a reader what came after its place, and waits at the end" $ do+      events <- newEventLog+      let one = paying ctx 1 [address 1]+          h1 = txHash one.tx+          block = BlockHash (txHash (paying ctx 2 []).tx).get+      start <- atomically (eventsEnd events)+      atomically $ do+        appendEvent events (TxEntered h1 (Just one))+        appendEvent events (BlockArrived block)+        appendEvent events (TxLeft h1)+      Just (Just read') <- atomically ((Just <$> readEvents events start) `orElse` return Nothing)+      map (.event) (toList read') `shouldBe` [EventTx h1, EventBlock block, EventTx h1]+      -- a transaction that leaves is known as it was when it entered+      map (.seen) (toList read') `shouldBe` [Just one, Nothing, Just one]+      end <- atomically (eventsEnd events)+      waiting <- atomically ((Just <$> readEvents events end) `orElse` return Nothing)+      fmap (fmap length) waiting `shouldBe` Nothing+    it "takes up after the first event about a hash, and not after one it does not have" $ do+      events <- newEventLog+      let seen n = paying ctx n [address 1]+          hashes = map (txHash . (.tx) . seen) [1 .. 3]+          h1 = hashes !! 0+          h2 = hashes !! 1+      atomically $ do+        mapM_ (\n -> appendEvent events (TxEntered (txHash (seen n).tx) (Just (seen n)))) [1 .. 3]+        appendEvent events (TxLeft h1)+      Just from <- atomically (eventsAfter events Nothing (Just h1))+      Just later <- atomically (readEvents events from)+      map (.event) (toList later) `shouldBe` map EventTx [h2, hashes !! 2, h1]+      -- a hash is that of a block or of a transaction, as the caller reads it+      atomically (eventsAfter events (Just (BlockHash h1.get)) Nothing) `shouldReturn` Nothing+      atomically (eventsAfter events Nothing (Just (txHash (seen 9).tx))) `shouldReturn` Nothing+    it "forgets the oldest events, and tells a reader that fell off the start" $ do+      events <- newEventLog+      let seen n = paying ctx n [address 1]+          enter n = appendEvent events (TxEntered (txHash (seen n).tx) (Just (seen n)))+          extra = 5+      atomically (mapM_ enter [1 .. fromIntegral logLength + extra])+      atomically (readEvents events 0) `shouldReturn` Nothing+      Just kept <- atomically (readEvents events (fromIntegral extra))+      length kept `shouldBe` logLength+      atomically (eventsAfter events Nothing (Just (txHash (seen 1).tx))) `shouldReturn` Nothing+      -- the transaction of a forgotten event is not known when it leaves+      atomically (appendEvent events (TxLeft (txHash (seen 1).tx)))+      end <- atomically (eventsEnd events)+      Just left <- atomically (readEvents events (end - 1))+      map (.seen) (toList left) `shouldBe` [Nothing]+    it "gets shorter when its transactions are large" $ do+      events <- newEventLog+      let large :: Word32 -> Seen+          large n =+            let s = paying ctx n [address 1]+             in Seen {tx = s.tx, addresses = s.addresses, size = logWeight `div` 4}+      atomically (mapM_ (\n -> appendEvent events (TxEntered (txHash (large n).tx) (Just (large n)))) [1 .. 10])+      end <- atomically (eventsEnd events)+      atomically (readEvents events (end - 5)) `shouldReturn` Nothing+      Just kept <- atomically (readEvents events (end - 4))+      length kept `shouldBe` 4++  describe "choice of a client" $ do+    let gap = 3 :: Word32+        unused _ = return [] :: IO [XPubBal]+        told c var seen = atomically (stateTVar var (relevant c gap seen))+    it "is everything until the client chooses, and again when it says so" $ do+      var <- newTVarIO everything+      told ctx var (Just (paying ctx 1 [address 1])) `shouldReturn` True+      command ctx gap unused var (EventWatch [address 2]) `shouldReturn` Right ()+      told ctx var (Just (paying ctx 1 [address 1])) `shouldReturn` False+      told ctx var (Just (paying ctx 2 [address 1, address 2])) `shouldReturn` True+      -- what the server does not know is told+      told ctx var Nothing `shouldReturn` True+      command ctx gap unused var (EventUnwatch [address 2]) `shouldReturn` Right ()+      told ctx var (Just (paying ctx 2 [address 1, address 2])) `shouldReturn` False+      command ctx gap unused var EventAll `shouldReturn` Right ()+      readTVarIO var `shouldReturn` everything+      -- blocks alone, which are events without a transaction+      command ctx gap unused var EventNone `shouldReturn` Right ()+      told ctx var (Just (paying ctx 1 [address 1])) `shouldReturn` False+      told ctx var Nothing `shouldReturn` True+    it "watches a key up to its gap, which moves on with its payments" $ do+      var <- newTVarIO everything+      let x = key ctx DeriveNormal+          pay chain i = Just (paying ctx (chain * 1000 + i) [keyAddress ctx x chain i])+      -- used up to the second address of the external chain+      command ctx gap (\_ -> return [used ctx x 0 1]) var (EventWatchXPub x) `shouldReturn` Right ()+      w <- readTVarIO var+      HashMap.lookup x w.keys `shouldBe` Just (5, 3)+      HashMap.size w.derived `shouldBe` 8+      told ctx var (pay 0 5) `shouldReturn` False+      told ctx var (pay 1 3) `shouldReturn` False+      told ctx var (pay 0 4) `shouldReturn` True+      -- the payment to the fifth address moved the gap to the eighth+      told ctx var (pay 0 7) `shouldReturn` True+      told ctx var (pay 0 11) `shouldReturn` False+      told ctx var (pay 1 2) `shouldReturn` True+      told ctx var (pay 1 5) `shouldReturn` True+      -- the same key with another type of address is another key+      let y = key ctx DeriveP2WPKH+          other = Just (paying ctx 77 [keyAddress ctx y 0 0])+      told ctx var other `shouldReturn` False+      command ctx gap unused var (EventWatchXPub y) `shouldReturn` Right ()+      told ctx var other `shouldReturn` True+      command ctx gap unused var (EventUnwatchXPub x) `shouldReturn` Right ()+      told ctx var (pay 0 0) `shouldReturn` False+      told ctx var other `shouldReturn` True+    it "matches a Bloom filter as a node does, next to the addresses" $ do+      var <- newTVarIO everything+      let element = \case+            PubKeyAddress h -> runPutS (serialize h)+            _ -> error "Not the hash of a key"+          filter' = bloomInsert (bloomCreate 10 0.0000001 0 BloomUpdateAll) (element (address 1))+          one = paying ctx 1 [address 1]+          two = paying ctx 2 [address 2]+          three = paying ctx 3 [address 3]+      command ctx gap unused var (EventFilterAdd "x") `shouldReturn` Left "No filter to add to"+      command ctx gap unused var (EventFilterLoad filter') `shouldReturn` Right ()+      command ctx gap unused var (EventWatch [address 3]) `shouldReturn` Right ()+      told ctx var (Just one) `shouldReturn` True+      told ctx var (Just two) `shouldReturn` False+      told ctx var (Just three) `shouldReturn` True+      -- the server does not add to the filter by itself+      (.bloom) <$> readTVarIO var `shouldReturn` Just filter'+      -- by the hash of the transaction, and by what the client adds+      command ctx gap unused var (EventFilterAdd (runPutS (serialize (txHash two.tx)))) `shouldReturn` Right ()+      told ctx var (Just two) `shouldReturn` True+      command ctx gap unused var EventFilterClear `shouldReturn` Right ()+      told ctx var (Just one) `shouldReturn` False+      told ctx var (Just three) `shouldReturn` True+    it "refuses more addresses or keys than a client may name" $ do+      var <- newTVarIO everything+      let numbered n = PubKeyAddress (addressHash (runPutS (serialize n)))+          many = map numbered [1 .. fromIntegral maxWatchAddresses :: Word32]+      command ctx gap unused var (EventWatch many) `shouldReturn` Right ()+      command ctx gap unused var (EventWatch [address 1]) `shouldReturn` Left "Too many addresses"+      -- a refused command changes nothing+      HashSet.size . (.plain) <$> readTVarIO var `shouldReturn` maxWatchAddresses
+ test/Haskoin/Store/Web/RouterSpec.hs view
@@ -0,0 +1,75 @@+module Haskoin.Store.Web.RouterSpec (spec) where++import Control.Exception (try)+import Data.ByteString.Builder (toLazyByteString)+import Data.ByteString.Lazy.Char8 qualified as L8+import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef)+import Data.Maybe (fromMaybe)+import Data.Text qualified as T+import Haskoin.Store.Web.Router+import Network.HTTP.Types (methodGet, parseQuery, status200, status404, statusCode)+import Network.Wai+import Network.Wai.Internal (Response (..), ResponseReceived (..))+import Test.Hspec++spec :: Spec+spec = describe "router" $ do+  it "answers with the route whose path matches, with its captures" $+    call "/block/abc" `shouldReturn` (200, "block abc")+  it "gives the parameters that the route accepts to its handler" $+    call "/block/abc?notx=true" `shouldReturn` (200, "block abc notx=true")+  it "refuses a parameter that the route does not accept, without running it" $+    call "/block/abc?limt=5" `shouldReturn` (404, "unknown limt")+  it "names every unknown parameter once" $+    call "/block/abc?a=1&b=2&a=3" `shouldReturn` (404, "unknown a,b")+  it "tries the next route when a handler passes, keeping its parameters" $+    call "/block/best?notx=false" `shouldReturn` (200, "best notx=false")+  it "falls back when no route has the path" $+    call "/nothing" `shouldReturn` (404, "no route")+  it "fails when a handler reads a parameter that its route does not declare" $ do+    r <- try (call "/sloppy")+    either (\(Undeclared n) -> n) (const "") r `shouldBe` "secret"++  it "answers inside the scope of a request, and makes one for each route that it tries" $ do+    events <- newIORef []+    let note e = modifyIORef' events (<> [e])+        scoped answer = note "open" >> answer routes <* note "close"+        req = defaultRequest {requestMethod = methodGet, pathInfo = ["block", "best"]}+    _ <- routerScoped routes scoped (\_ _ -> fail "no route") req $ \_ ->+      note "respond" >> return ResponseReceived+    -- the first route passes the request on, the second answers it+    readIORef events `shouldReturn` ["open", "close", "open", "respond", "close" :: String]++-- | Routes for the tests: the first matches any block but passes on+-- "best", for the second to answer.+routes :: [Route]+routes =+  [ get "/block/:block" ["notx"] $ \req -> do+      b <- capture req "block"+      if b == "best" then next else notx req >>= \n -> ok (T.unwords (["block", b] <> n)),+    get "/block/best" ["notx"] $ \req -> notx req >>= \n -> ok (T.unwords ("best" : n)),+    get "/sloppy" [] $ \req -> param req "secret" >>= ok . fromMaybe ""+  ]+  where+    notx req = maybe [] (\v -> ["notx=" <> v]) <$> param req "notx"+    ok t = return (responseLBS status200 [] (L8.pack (T.unpack t)))++-- | Status and body of the answer to a GET of a path with a query string.+call :: String -> IO (Int, String)+call url = do+  let (path, query) = break (== '?') url+      req =+        defaultRequest+          { requestMethod = methodGet,+            pathInfo = filter (not . T.null) (T.splitOn "/" (T.pack path)),+            queryString = parseQuery (L8.toStrict (L8.pack query))+          }+  out <- newIORef Nothing+  _ <- router routes miss req $ \res -> writeIORef out (Just res) >> return ResponseReceived+  readIORef out >>= \case+    Just (ResponseBuilder s _ b) -> return (statusCode s, L8.unpack (toLazyByteString b))+    _ -> fail "no response"+  where+    miss _ NoRoute = return (responseLBS status404 [] "no route")+    miss _ (Unknown ks) =+      return (responseLBS status404 [] (L8.pack ("unknown " <> T.unpack (T.intercalate "," ks))))
test/Haskoin/Store/WebSpec.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}- module Haskoin.Store.WebSpec (spec) where  import Data.ByteString (ByteString)@@ -8,7 +5,7 @@ import Data.Maybe (fromJust) import Data.Text.Encoding (decodeUtf8) import Haskoin (Tx (..), bch, btc, decodeHex, txHash, txHashToHex)-import Haskoin.Store.Web (decodeTxBody)+import Haskoin.Store.Web (acceptsBinary, decodeTxBody) import Test.Hspec  -- | A transaction written by hand, in the format without witness. It has@@ -35,7 +32,25 @@   \00000000"  spec :: Spec-spec =+spec = do+  describe "acceptsBinary" $ do+    it "takes the binary type by itself" $+      acceptsBinary "application/octet-stream" `shouldBe` True+    it "takes it in a list, in any case and with spaces" $ do+      acceptsBinary "text/html, Application/Octet-Stream" `shouldBe` True+      acceptsBinary "application/octet-stream , */*" `shouldBe` True+    it "takes it with a weight above zero" $+      acceptsBinary "application/octet-stream;q=0.5" `shouldBe` True+    it "gives JSON to a wildcard, to nothing and to JSON" $ do+      acceptsBinary "*/*" `shouldBe` False+      acceptsBinary "" `shouldBe` False+      acceptsBinary "application/json" `shouldBe` False+    it "gives JSON when binary is refused with a weight of zero" $+      acceptsBinary "application/octet-stream;q=0, application/json" `shouldBe` False+    it "follows the weights when both are named" $ do+      acceptsBinary "application/json;q=0.9, application/octet-stream" `shouldBe` True+      acceptsBinary "application/json, application/octet-stream;q=0.9" `shouldBe` False+      acceptsBinary "application/json;q=0.5, application/octet-stream; q=0.5" `shouldBe` True   describe "decodeTxBody" $ do     -- The id is the double SHA-256 of the bytes of plainTxHex, reversed. It     -- was computed outside this library. The witness is not part of the id,
test/Haskoin/StoreSpec.hs view
@@ -1,17 +1,9 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.StoreSpec (spec) where  import Conduit import Control.Monad-import Control.Monad.Reader import Data.ByteString (ByteString) import Data.ByteString qualified as B import Data.ByteString.Base64@@ -22,13 +14,14 @@ import Data.Time.Clock.POSIX import Data.Word import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)-import System.Mem (performMajorGC) import Haskoin import Haskoin.Node import Haskoin.Store+import Haskoin.Store.Database.Query qualified as Q import Haskoin.Store.TestBlocks (nextBlock) import NQE import Network.Socket+import System.Mem (performMajorGC) import System.Random import Test.Hspec import UnliftIO@@ -51,14 +44,14 @@           StoreBestBlock b -> Just b           _ -> Nothing         let bestHash = last bs-        bestNodeM <- liftIO $ chainGetBlock testStoreChain bestHash+        bestNodeM <- chainGetBlock testStoreChain bestHash         bestNodeM `shouldSatisfy` isJust         let bestNode = fromJust bestNodeM             bestHeight = bestNode.height         bestHeight `shouldBe` 8     it "get a block and its transactions" $       withTestStore bchRegTest ctx "get-block-txs" $ \TestStore {..} ->-        flip runReaderT testStoreDB $ do+        do           let h1 = "5369ef2386c72acdf513ffd80aeba2a1774e2f004d120761e54a8bf614173f3e"               get_the_block h =                 receive testStoreEvents >>= \case@@ -68,20 +61,19 @@                         get_the_block ((h :: Int) - 1)                   _ -> get_the_block h           bh <- get_the_block 15-          m <- getBlock bh+          m <- Q.getBlock testStoreDB bh           let bd = fromMaybe (error "Could not get block") m-          t1 <- getTransaction h1-          lift $ do-            bd.height `shouldBe` 15-            length bd.txs `shouldBe` 1-            listToMaybe bd.txs `shouldBe` Just h1-            fmap (txHash . transactionData) t1 `shouldBe` Just h1+          t1 <- Q.getTxData testStoreDB h1+          bd.height `shouldBe` 15+          length bd.txs `shouldBe` 1+          listToMaybe bd.txs `shouldBe` Just h1+          fmap (txHash . (.tx)) t1 `shouldBe` Just h1     it "downloads 80 blocks with a request for 64 and one for the remaining 16" $       withTestStore bchRegTest ctx "block-window" $ \TestStore {..} -> do         bs <- replicateM 80 . receiveMatch testStoreEvents $ \case           StoreBestBlock b -> Just b           _ -> Nothing-        bestNodeM <- liftIO $ chainGetBlock testStoreChain (last bs)+        bestNodeM <- chainGetBlock testStoreChain (last bs)         fmap (.height) bestNodeM `shouldBe` Just 80         readTVarIO testStoreBlockRequests `shouldReturn` [64, 16] @@ -117,23 +109,21 @@         growth `shouldSatisfy` (< 1024 * 1024)  withTestStore ::-  (MonadUnliftIO m) =>   Network ->   Ctx ->   String ->-  (TestStore -> m a) ->-  m a+  (TestStore -> IO a) ->+  IO a withTestStore = withTestStoreBlocks allBlocks  -- | Like 'withTestStore', with the mock peer serving these blocks. withTestStoreBlocks ::-  (MonadUnliftIO m) =>   [Block] ->   Network ->   Ctx ->   String ->-  (TestStore -> m a) ->-  m a+  (TestStore -> IO a) ->+  IO a withTestStoreBlocks blocks net ctx t f =   withSystemTempDirectory ("haskoin-store-test-" <> t <> "-") $ \w -> do     requests <- newTVarIO []@@ -144,8 +134,10 @@         cfg =           StoreConfig             { maxPeers = 20,-              initPeers = [],-              discover = True,+              -- the mock peer, by name: since haskoin-node 2.5 the seed of+              -- a regtest network is not an address that discovery takes+              initPeers = ["127.0.0.1:18444"],+              discover = False,               db = w,               net = net,               ctx = ctx,@@ -153,9 +145,11 @@               gap = gap,               initGap = 20,               redisMinAddrs = 100,-              redisMaxKeys = 100 * 1000 * 1000,+              redisMinTxs = 1000,+              redisMaxXPubs = 10000,               noMempool = False,               wipeMempool = False,+              fleet = False,               syncMempool = False,               blockWindow = 64,               mempoolTimeout = 14,@@ -163,7 +157,6 @@               maxPeerLife = 48 * 3600,               connect = dummyPeerConnect blocks requests net ad,               stats = Nothing,-              redisSyncInterval = 30,               bloom = True,               maxOpenFiles = Nothing,               writeBufferSize = Nothing,