packages feed

haskoin-store 1.14.0 → 1.15.0

raw patch · 14 files changed

+418/−57 lines, 14 filesdep ~haskoin-coredep ~haskoin-store-dataPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: haskoin-core, haskoin-store-data

API changes (from Hackage documentation)

+ Haskoin.Store: [getManyTxBytes] :: StoreRead -> [TxHash] -> IO [Maybe ByteString]
+ Haskoin.Store.Common: [getManyTxBytes] :: StoreRead -> [TxHash] -> IO [Maybe ByteString]
+ Haskoin.Store.Database.Query: getManyTxBytes :: DatabaseReader -> [TxHash] -> IO [Maybe ByteString]
+ Haskoin.Store.Database.Query: liveTxBytes :: ByteString -> Maybe ByteString
+ Haskoin.Store.Database.Query: retrieveManyBytesCF :: Serialize key => DB -> ColumnFamily -> [key] -> IO [Maybe ByteString]
+ Haskoin.Store.Database.Query.Layered: getManyTxBytes :: View -> [TxHash] -> IO [Maybe ByteString]
+ Haskoin.Store.Database.Slices: blockData :: ByteString -> Either String BlockData
+ Haskoin.Store.Database.Slices: txBytes :: ByteString -> Either String (ByteString, Bool)
- 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) -> (TxHash -> IO (Maybe Replaced)) -> StoreRead
+ Haskoin.Store: StoreRead :: !Network -> !Ctx -> IO (Maybe BlockHash) -> (BlockHeight -> IO [BlockHash]) -> (BlockHash -> IO (Maybe BlockData)) -> (TxHash -> IO (Maybe TxData)) -> ([TxHash] -> IO [Maybe TxData]) -> ([TxHash] -> IO [Maybe ByteString]) -> (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) -> (TxHash -> IO (Maybe Replaced)) -> StoreRead
- 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) -> (TxHash -> IO (Maybe Replaced)) -> StoreRead
+ Haskoin.Store.Common: StoreRead :: !Network -> !Ctx -> IO (Maybe BlockHash) -> (BlockHeight -> IO [BlockHash]) -> (BlockHash -> IO (Maybe BlockData)) -> (TxHash -> IO (Maybe TxData)) -> ([TxHash] -> IO [Maybe TxData]) -> ([TxHash] -> IO [Maybe ByteString]) -> (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) -> (TxHash -> IO (Maybe Replaced)) -> StoreRead

Files

CHANGELOG.md view
@@ -4,6 +4,43 @@ 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) +## [1.15.0] - 2026-11-14++Answers decode less of what they read, and write hex faster.++### Changed+- **Raw answers send the transactions as stored.** A raw transaction,+  raw block, or the raw transactions of a block, were decoded, built into+  their JSON form, with addresses and ids, turned back and serialized+  again; and the spenders of their outputs were read for nothing. Now the+  bytes of each transaction are found in its stored value and written as+  they are. The raw block of 3,011 transactions of the load test takes 21+  ms of CPU where it took 60, and its raw transactions 24 ms where they+  took 65. This covers `/transaction/:txid/raw`, `/transactions/raw`,+  `/transactions/block/:block/raw`, the five `/block/…/raw` routes, and+  the hex of `/blockchain/rawtx` and `/blockchain/rawblock`.+- **A block's list of transactions is decoded when it is used.** Reading+  a block decoded the hashes of all its transactions, 180 µs for a block+  of 3,000, for the many reads that want its height or its header: the+  health check, the best block of a request, the median time of a new+  transaction, blocks without their transactions. `/block/best?notx=true`+  on a block of 3,011 transactions takes 0.06 ms of CPU where it took+  0.24, and `/block/latest?notx=true` 31% less.+- **Transactions in JSON are built without serializing them.** Their size+  and weight are counted from their parts, and the id of a transaction+  read by its id is not computed again (haskoin-store-data's+  `toTransactionWithId`). With the faster hex of haskoin-core, 5,000 full+  transactions of an address take 20% less CPU, and the 3,011 of a block+  21% less.+- **Hex is written faster** (haskoin-core): from a table of digit pairs,+  and the hashes of transactions and blocks in one step, five times as+  fast. A list of 100 transactions of an address takes 12% less CPU.+- `StoreRead` has `getManyTxBytes`, the transactions as stored, and the+  new module `Haskoin.Store.Database.Slices` reads stored values in part.+- The load test's last block holds 3,000 more transactions, when the chain+  has 400 blocks or more, and `scripts/bench-web.sh` measures the raw+  answers.+ ## [1.14.0] - 2026-11-14  Long answers are encoded on several cores, addresses are written faster,
README.md view
@@ -733,6 +733,7 @@ | `Haskoin.Store.Logic.Layered` | The rules that import blocks and transactions | | `Haskoin.Store.Database.Mempool` | The keys of the mempool area, and what it does to balances | | `Haskoin.Store.Database.Query.Layered` | The queries over both areas, each on a snapshot |+| `Haskoin.Store.Database.Slices` | Stored blocks and transactions read in part, for what an answer needs | | `Haskoin.Store.Database.Migrate` | The migrations from versions 20 and 21 | | `Haskoin.Store.Cache` | The Redis cache of a server alone | | `Haskoin.Store.Fleet.Member`, `Haskoin.Store.Fleet.XPub` | A fleet, and its cache |
bench/Main.hs view
@@ -13,7 +13,8 @@ -- output of each transaction pay the "hot" address, so it gains eleven -- transactions per block. Nine outputs per block go to the first 100 -- external and change addresses of the benchmark xpub, in turn. Both are--- printed once the chain is synced.+-- printed once the chain is synced. With 400 blocks or more, the last one+-- also holds 3,000 transactions that spend outputs of the hot address. module Main (main) where  import Conduit@@ -175,18 +176,32 @@ scriptOf :: Ctx -> Address -> ByteString scriptOf ctx = fromMaybe (error "script") . addressToScriptBS ctx --- | Blocks at heights 1 to n on top of the regtest genesis.+-- | Blocks at heights 1 to n on top of the regtest genesis. The last block+-- of a chain of 400 blocks or more also spends 3,000 outputs that earlier+-- blocks paid to the hot address, one transaction each, so that there is a+-- large block to answer about. chain :: Ctx -> Network -> Int -> [Block]-chain ctx net n = go 1 net.genesisHeader Nothing+chain ctx net n = go 1 net.genesisHeader   where     hot = scriptOf ctx (hotAddr ctx)-    go h parent prevcb+    coinbase h = Tx 1 [TxIn nullOutPoint (encode (fromIntegral h :: Word32)) maxBound] [TxOut 5000000000 hot] [] 0+    -- the transactions of the block at a height, after its coinbase+    regular h+      | h <= 1 = []+      | otherwise = spends h (txHash (coinbase (h - 1)))+    go h parent       | h > n = []       | otherwise =-          let cb = Tx 1 [TxIn nullOutPoint (encode (fromIntegral h :: Word32)) maxBound] [TxOut 5000000000 hot] [] 0-              txs = maybe [] (spends h) prevcb-              block = mine net parent (cb : txs)-           in block : go (h + 1) block.header (Just (txHash cb))+          let extra = if h == n && n >= 400 then wide else []+              block = mine net parent (coinbase h : regular h <> extra)+           in block : go (h + 1) block.header+    -- the outputs to the hot address of the transactions of blocks 2 to+    -- 335, nine a block, each spent by a transaction of its own+    wide =+      [ Tx 1 [TxIn (OutPoint (txHash t) 1) "" maxBound] [TxOut 98990000 hot] [] 0+      | h <- [2 .. 335],+        t <- drop 1 (regular h)+      ]     spends h cb =       let t1 = Tx 1 [TxIn (OutPoint cb 0) "" maxBound] (replicate 9 (TxOut 500000000 hot)) [] 0           tk k =
haskoin-store.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           haskoin-store-version:        1.14.0+version:        1.15.0 synopsis:       Storage and index for Bitcoin and Bitcoin Cash 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@@ -63,6 +63,7 @@       Haskoin.Store.Database.Query.Layered       Haskoin.Store.Database.Query.Pooled       Haskoin.Store.Database.Reader+      Haskoin.Store.Database.Slices       Haskoin.Store.Database.Types       Haskoin.Store.Database.Views       Haskoin.Store.Database.Writer@@ -108,9 +109,9 @@     , 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.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , http-types >=0.12.6 && <0.13     , lens >=5.3.6 && <5.4@@ -172,9 +173,9 @@     , 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.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , http-types >=0.12.6 && <0.13     , lens >=5.3.6 && <5.4@@ -234,10 +235,10 @@     , filepath >=1.5.5.0 && <1.6     , hashable >=1.5.1.0 && <1.6     , hashtables >=1.4.2 && <1.5-    , haskoin-core >=1.6.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6     , haskoin-store-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , http-types >=0.12.6 && <0.13     , lens >=5.3.6 && <5.4@@ -278,6 +279,7 @@       Haskoin.Store.CommonSpec       Haskoin.Store.Database.MigrateSpec       Haskoin.Store.Database.Query.LayeredSpec+      Haskoin.Store.Database.SlicesSpec       Haskoin.Store.Database.ViewsSpec       Haskoin.Store.Fleet.MemberSpec       Haskoin.Store.Fleet.PoolSpec@@ -325,10 +327,10 @@     , 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.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6     , haskoin-store-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , hspec >=2.11.18 && <2.12     , http-types >=0.12.6 && <0.13@@ -391,10 +393,10 @@     , 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.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6     , haskoin-store-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , http-types >=0.12.6 && <0.13     , lens >=5.3.6 && <5.4@@ -454,10 +456,10 @@     , 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.3 && <1.7+    , haskoin-core >=1.6.4 && <1.7     , haskoin-node >=2.5.1 && <2.6     , haskoin-store-    , haskoin-store-data >=2.0.1 && <2.1+    , haskoin-store-data >=2.0.2 && <2.1     , hedis >=0.16.4 && <0.17     , http-types >=0.12.6 && <0.13     , integration
src/Haskoin/Store/Common.hs view
@@ -113,6 +113,7 @@     XPubUnspent (..),     nullBalance,     toTransaction,+    toTransactionWithId,     zeroBalance,   ) @@ -184,6 +185,11 @@     -- | data of many transactions, in the order of their hashes, read     -- together     getManyTxData :: [TxHash] -> IO [Maybe TxData],+    -- | many transactions as stored, in the format of the network, in the+    -- order of their hashes, read together, and left out where+    -- 'getManyTxData' leaves them out. Nothing of them is decoded, and the+    -- spenders of their outputs are not read: this is for raw answers+    getManyTxBytes :: [TxHash] -> IO [Maybe ByteString],     -- | 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@@ -413,11 +419,12 @@  -- | Transaction with its metadata, or 'Nothing' if it is unknown. getTransaction :: StoreRead -> TxHash -> IO (Maybe Transaction)-getTransaction r h = fmap (toTransaction r.net r.ctx) <$> r.getTxData h+getTransaction r h = fmap (toTransactionWithId r.net r.ctx h) <$> r.getTxData h --- | Many transactions, in the order of their hashes, read together.+-- | Many transactions, in the order of their hashes, read together. Each is+-- read by its id, so its id is not computed again ('toTransactionWithId'). getTransactions :: StoreRead -> [TxHash] -> IO [Maybe Transaction]-getTransactions r hs = map (fmap (toTransaction r.net r.ctx)) <$> r.getManyTxData hs+getTransactions r hs = zipWith (fmap . toTransactionWithId r.net r.ctx) hs <$> r.getManyTxData hs  -- | Transactions that match a short numeric identifier; see 'getNumTxData'. getNumTransaction :: StoreRead -> Word64 -> IO [Transaction]
src/Haskoin/Store/Database/Query.hs view
@@ -42,6 +42,8 @@     getTxData,     getTxRecord,     getManyTxData,+    getManyTxBytes,+    liveTxBytes,     getChainSpender,     getChainSpenders,     withChainSpenders,@@ -77,6 +79,7 @@     count,     withManyIters,     retrieveManyCF,+    retrieveManyBytesCF,     joinConduits,     usedAddresses,   )@@ -98,6 +101,7 @@   ) import Control.Monad (forM, (>=>)) import Data.Bits ((.&.))+import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.Default (def) import Data.Functor ((<&>))@@ -156,6 +160,7 @@   ) import Haskoin.Store.Data import Haskoin.Store.Database.Reader+import Haskoin.Store.Database.Slices (blockData, txBytes) import Haskoin.Store.Database.Types import System.Metrics.StatsD (decrementGauge, incrementCounter, incrementGauge) import UnliftIO@@ -182,6 +187,7 @@       getBlock = getBlock r,       getTxData = fmap notDeleted . getTxData r,       getManyTxData = fmap (map notDeleted) . getManyTxData r,+      getManyTxBytes = getManyTxBytes r,       getSpender = getSpender r,       getBalance = getBalance r,       getUnspent = getUnspent r,@@ -236,14 +242,16 @@   rows <- matchingAsListCF r.db (heightCF r.db) (ReplacedAtKeyH h)   return [t | (ReplacedAtKey _ t, ()) <- rows] --- | Data of a block.+-- | Data of a block. Its list of transactions is decoded when it is used,+-- as "Haskoin.Store.Database.Slices" explains: most reads of a block want+-- its height or its header. getBlock :: DatabaseReader -> BlockHash -> IO (Maybe BlockData) getBlock r h =-  retrieveCF r.db (blockCF r.db) (BlockKey h) >>= \case+  getCF r.db (blockCF r.db) (encode (BlockKey h)) >>= \case     Nothing -> return Nothing-    Just b -> do+    Just bytes -> do       count r $ \s -> incrementCounter s.dataBlockCount 1-      return (Just b)+      return (Just (either error id (blockData bytes)))  ------------------------------------------------------------------------------- -- Transactions@@ -280,6 +288,22 @@     fill (Nothing : more) tds = Nothing : fill more tds     fill _ _ = [] +-- | Transactions as stored, in the format of the network, without the+-- deleted ones: slices of their values ("Haskoin.Store.Database.Slices"),+-- without the spenders of their outputs.+getManyTxBytes :: DatabaseReader -> [TxHash] -> IO [Maybe ByteString]+getManyTxBytes r ths = do+  found <- map (>>= liveTxBytes) <$> retrieveManyBytesCF r.db (txCF r.db) (map TxKey ths)+  count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes found))+  return found++-- | The transaction of a stored 'TxData', unless it is deleted.+liveTxBytes :: ByteString -> Maybe ByteString+liveTxBytes value = case txBytes value of+  Right (bytes, False) -> Just bytes+  Right (_, True) -> Nothing+  Left e -> error ("liveTxBytes: " <> e)+ -- | 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)@@ -698,10 +722,14 @@ -- 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)+retrieveManyCF db cf keys = map (fmap (either error id . decode)) <$> retrieveManyBytesCF db cf keys++-- | The bytes of the values of many keys, as 'retrieveManyCF' reads them,+-- for values that are read in part ("Haskoin.Store.Database.Slices").+retrieveManyBytesCF :: (Serialize key) => DB -> ColumnFamily -> [key] -> IO [Maybe ByteString]+retrieveManyBytesCF _ _ [] = return []+retrieveManyBytesCF db cf [key] = pure <$> getCF db cf (encode key)+retrieveManyBytesCF db cf keys = multiGetCF db cf (map encode keys)  -- | Merge streams sorted newest first, drop duplicates, and apply the offset -- and limit.
src/Haskoin/Store/Database/Query/Layered.hs view
@@ -79,6 +79,7 @@     -- * Transactions     getTxData,     getManyTxData,+    getManyTxBytes,     getNumTxData,     getSpender, @@ -119,6 +120,7 @@   ) import Control.Monad (forM) import Data.Bits ((.&.))+import Control.Applicative ((<|>)) import Data.ByteString qualified as BS import Data.Functor ((<&>)) import Data.HashMap.Strict (HashMap)@@ -243,6 +245,7 @@       getBlock = getBlock v,       getTxData = fmap notDeleted . getTxData v,       getManyTxData = fmap (map notDeleted) . getManyTxData v,+      getManyTxBytes = getManyTxBytes v,       getSpender = getSpender v,       getBalance = getBalance v,       getUnspent = getUnspent v,@@ -279,6 +282,7 @@       getBlock = \h -> one (\q -> q.getBlock h),       getTxData = \h -> one (\q -> q.getTxData h),       getManyTxData = \hs -> one (\q -> q.getManyTxData hs),+      getManyTxBytes = \hs -> one (\q -> q.getManyTxBytes hs),       getSpender = \p -> one (\q -> q.getSpender p),       getBalance = \a -> one (\q -> q.getBalance a),       getUnspent = \p -> one (\q -> q.getUnspent p),@@ -343,6 +347,20 @@   let tds = [withSpenders spent h <$> Map.lookup h found | h <- hs]   Q.count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes tds))   return tds++-- | Transactions as stored, in the format of the network, from the mempool+-- area or the chain, as 'getManyTxData' finds them, without the deleted+-- ones and without reading the spenders of their outputs.+getManyTxBytes :: View -> [TxHash] -> IO [Maybe BS.ByteString]+getManyTxBytes (View r) hs = do+  unconfirmed <- look (mempoolCF r.db) hs+  chain <- look (txCF r.db) (filter (`Map.notMember` unconfirmed) hs)+  let found = [Map.lookup h unconfirmed <|> Map.lookup h chain >>= Q.liveTxBytes | h <- hs]+  Q.count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes found))+  return found+  where+    look _ [] = return Map.empty+    look cf ks = Map.fromList . catMaybes . zipWith (fmap . (,)) ks <$> Q.retrieveManyBytesCF r.db cf (map TxKey ks)  -- | 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
src/Haskoin/Store/Database/Query/Pooled.hs view
@@ -61,6 +61,9 @@   ) import Control.Monad (forM) import Data.Bits ((.&.))+import Control.Applicative ((<|>))+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize) import Data.ByteString qualified as BS import Data.Functor ((<&>)) import Data.HashMap.Strict qualified as HashMap@@ -179,6 +182,7 @@       getBlock = Q.getBlock r,       getTxData = \h -> only <$> getManyTxData r pool [h],       getManyTxData = getManyTxData r pool,+      getManyTxBytes = getManyTxBytes r pool,       getSpender = getSpender r pool,       getBalance = getBalance r pool,       getUnspent = getUnspent r pool,@@ -221,6 +225,20 @@ getManyTxData r pool hs = do   found <- known r pool hs   return [Map.lookup h found | h <- hs]++-- | Transactions in the format of the network: those that the pool has,+-- serialized from what it gives, and those that the chain has, as stored,+-- for the others, without the deleted ones and without spenders.+getManyTxBytes :: DatabaseReader -> PoolReads -> [TxHash] -> IO [Maybe BS.ByteString]+getManyTxBytes _ _ [] = return []+getManyTxBytes r pool hs = do+  inPool <- zip hs <$> pool.bodies hs+  let unconfirmed = Map.fromList [(h, runPutS (serialize td.tx)) | (h, (Just td, _)) <- inPool]+      rest = filter (`Map.notMember` unconfirmed) hs+  chain <-+    Map.fromList . catMaybes . zipWith (\h v -> (,) h <$> (v >>= Q.liveTxBytes)) rest+      <$> Q.retrieveManyBytesCF r.db (txCF r.db) (map TxKey rest)+  return [Map.lookup h unconfirmed <|> Map.lookup h chain | 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
+ src/Haskoin/Store/Database/Slices.hs view
@@ -0,0 +1,152 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Haskoin.Store.Database.Slices+-- Copyright   : No rights reserved+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- Stored values read in part: what an answer needs, and nothing more.+--+-- A value of the database is decoded whole, as a rule, into the type that+-- it stores. Two of them are often read for a small part of what they hold:+--+-- * A block ('BlockData') holds the hash of every one of its transactions.+--   Most reads of a block want its height, its header or its time: the+--   health check, the best block of a request, a block without its+--   transactions. Decoding the hashes took 180 µs for a block of 3,000+--   transactions and 2.4 ms for one of 20,000, where the rest took less+--   than a microsecond.+--+-- * A transaction ('TxData') holds the transaction in the format of the+--   network, which is what a raw answer sends. Building the transaction to+--   serialize it again took ten times as long as finding its bytes, and+--   read the spenders of its outputs, which a raw answer does not need.+--+-- 'blockData' gives the block whole, with its list of transactions decoded+-- the first time that it is used: the type stays the same, and every caller+-- gains. 'txBytes' gives the bytes of the transaction, a slice of the value,+-- found by skipping over the parts of the transaction rather than building+-- them. Both are held to the full decoders by the tests.+module Haskoin.Store.Database.Slices+  ( blockData,+    txBytes,+  )+where++import Control.Monad (replicateM, replicateM_, unless, when)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Bytes.Get (runGetS)+import Data.Bytes.Serial (deserialize)+import Data.Serialize.Get+  ( Get,+    getBytes,+    getWord32be,+    getWord64be,+    getWord8,+    lookAhead,+    remaining,+    runGet,+    skip,+  )+import Haskoin (BlockHeader, TxHash, VarInt (..), getInteger)+import Haskoin.Store.Data (BlockData (..), BlockRef)++-- | A block as stored, decoded with its list of transactions left as bytes+-- until it is used. The list is then decoded at once, as the full decoder+-- would have: what the block holds is the same either way. Its bytes were+-- checked to be there, so decoding them later cannot fail.+blockData :: ByteString -> Either String BlockData+blockData = runGet $ do+  height <- getWord32be+  main <- deserialize+  work <- getInteger+  header <- deserialize :: Get BlockHeader+  size <- getWord32be+  weight <- getWord32be+  n <- count+  left <- remaining+  when (n > left `div` 32) $ fail "blockData: fewer bytes than transactions"+  hashes <- getBytes (32 * n)+  outputs <- getWord64be+  fee <- getWord64be+  subsidy <- getWord64be+  return BlockData {txs = later n hashes, ..}+  where+    -- the first cell of the list is made at once, as the field is strict;+    -- its head, the coinbase, which a block without its transactions still+    -- shows, is decoded by itself, and the tail all at once when it is used+    later :: Int -> ByteString -> [TxHash]+    later 0 _ = []+    later n hashes =+      decoded deserialize (B.take 32 hashes)+        : decoded (replicateM (n - 1) deserialize) (B.drop 32 hashes)+    decoded :: Get a -> ByteString -> a+    decoded parser bytes = either error id (runGetS parser bytes)++-- | The transaction of a stored 'TxData', in the format of the network, as+-- 'Data.Bytes.Serial.serialize' wrote it there, and whether the transaction+-- is deleted. The parts of the transaction are skipped over by their+-- lengths, as the decoder of haskoin-core reads them, without building+-- them; the checks of that decoder were made when the value was written.+txBytes :: ByteString -> Either String (ByteString, Bool)+txBytes value = flip runGet value $ do+  _ <- deserialize :: Get BlockRef+  before <- remaining+  skipTx+  after <- remaining+  -- the outputs that the inputs spend: a key and a script and a value each+  entries <- count+  replicateM_ entries (skip 8 >> count >>= skip >> skip 8)+  deleted <- deserialize+  let start = B.length value - before+  return (B.take (before - after) (B.drop start value), deleted)++-- | Skip a transaction in the format of the network: the old one, or the+-- one with a witness of BIP 144, told apart as the decoder of haskoin-core+-- tells them, by the flags after a marker of zero.+skipTx :: Get ()+skipTx = do+  skip 4+  flags <- lookAhead $ do+    marker <- getWord8+    if marker == 0 then Just <$> getWord8 else return Nothing+  case flags of+    Just 0x01 -> do+      skip 2+      inputs <- varCount+      replicateM_ inputs input+      varCount >>= flip replicateM_ output+      replicateM_ inputs (varCount >>= flip replicateM_ (varCount >>= skip))+      skip 4+    Just 0x00 -> legacy+    Nothing -> legacy+    Just other -> fail ("txBytes: unknown flags " <> show other)+  where+    legacy = do+      varCount >>= flip replicateM_ input+      varCount >>= flip replicateM_ output+      skip 4+    -- an outpoint, a script and a sequence+    input = skip 36 >> varCount >>= skip >> skip 4+    -- a value and a script+    output = skip 8 >> varCount >>= skip++-- | A count of the network's format, as a variable integer.+varCount :: Get Int+varCount = do+  VarInt n <- deserialize+  unless (n <= fromIntegral (maxBound :: Int)) $ fail "txBytes: count too large"+  return (fromIntegral n)++-- | A 64-bit count of the store's format, that must fit an 'Int'.+count :: Get Int+count = do+  n <- getWord64be+  unless (n <= fromIntegral (maxBound :: Int)) $ fail "count too large"+  return (fromIntegral n)
src/Haskoin/Store/Web.hs view
@@ -125,7 +125,8 @@   ) import Data.Aeson.Types (parseMaybe) import Data.ByteString.Base16 (decodeBase16, parseBase16)-import Data.ByteString.Builder (lazyByteString)+import Data.ByteString (ByteString)+import Data.ByteString.Builder (Builder, lazyByteString) import Data.ByteString.Char8 qualified as C import Data.ByteString.Lazy qualified as L import Data.Bytes.Get@@ -139,7 +140,7 @@ import Data.HashSet qualified as HashSet import Data.Int (Int64) import Data.List qualified-import Data.List (nub)+import Data.List (intersperse, nub) import Data.Maybe   ( catMaybes,     fromJust,@@ -256,6 +257,7 @@     getRequestBodyChunk,     mapResponseHeaders,     modifyResponse,+    responseBuilder,     responseLBS,     responseStatus,     responseStream,@@ -1063,7 +1065,7 @@     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),+    pieces (.txRaw) [] (\r -> GetTxRaw <$> cap r) (handleTxRaw st),     pathPosted [] (\r -> PostTx <$> parseTxBody st r) (handlePostTx st),     plainList limits (\r -> GetAddrTxs <$> cap r <*> lim r) (handleAddrTxs st),     marshalled [] (\r -> GetAddrBalance <$> cap r) (handleAddrBalance st),@@ -1080,7 +1082,7 @@     pieces (.blockRaw) [] (\r -> GetBlockTimeRaw <$> cap r) (handleBlockTimeRaw st),     pieces (.blockRaw) [] (\r -> GetBlockMTPRaw <$> cap r) (handleBlockMTPRaw st),     marshalledList ["txids"] (\r -> GetTxs <$> need r) (handleTxs st),-    plain ["txids"] (\r -> GetTxsRaw <$> need r) (handleTxsRaw st),+    pieces (.txRaw) ["txids"] (\r -> GetTxsRaw <$> need r) (handleTxsRaw st),     marshalledPieces (.txBlock) [] (\r -> GetTxsBlock <$> cap r) (handleTxsBlock st),     pieces (.txBlockRaw) [] (\r -> GetTxsBlockRaw <$> cap r) (handleTxsBlockRaw st),     plain [] (\r -> GetTxAfter <$> cap r <*> cap r) (handleTxAfter st),@@ -1362,8 +1364,14 @@ rawBlock :: StoreRead -> BlockData -> [Piece] rawBlock q b =   around (hexOf b.header <> hexOf (VarInt (fromIntegral (length b.txs)))) mempty $-    map piece (pieced pieceSize (fmap (foldMap (hexOf . transactionData) . catMaybes) . C.getTransactions q) b.txs)+    map piece (pieced pieceSize (fmap (foldMap rawHex . catMaybes) . q.getManyTxBytes) b.txs) +-- | The hex of bytes as stored, such as a transaction in the format of the+-- network: read as they are, and written with the table of haskoin-core's+-- 'hexBuilder'.+rawHex :: ByteString -> Builder+rawHex = hexBuilder . L.fromStrict+ -- | The pieces of the JSON of a 'RawResult', from those of the hexadecimal. rawResult :: [Piece] -> [Piece] rawResult = around "{\"result\":\"" "\"}"@@ -1499,23 +1507,24 @@ handleTxRaw ::   WebState ->   GetTxRaw ->-  IO (RawResult Tx)+  IO [Piece] handleTxRaw st (GetTxRaw txid) =-  withMetrics st (.txRaw) $-    C.getTransaction st.query txid-      >>= \case-        Nothing -> notFoundTx st txid-        Just tx -> return $ RawResult (transactionData tx)+  st.query.getManyTxBytes [txid] >>= \case+    [Just bytes] -> return (rawResult [piece (return (rawHex bytes))])+    _ -> notFoundTx st txid --- | Handler for @GET \/transactions\/raw?txids=…@: several transactions, serialized.+-- | Handler for @GET \/transactions\/raw?txids=…@: several transactions, serialized. The client names+-- them, so they are all read before the answer begins, in one piece. handleTxsRaw ::   WebState ->   GetTxsRaw ->-  IO (RawResultList Tx)-handleTxsRaw st (GetTxsRaw txids) =-  withMetrics st (.txRaw) $-    RawResultList . map transactionData . catMaybes-      <$> C.getTransactions st.query (nub txids)+  IO [Piece]+handleTxsRaw st (GetTxsRaw txids) = do+  found <- catMaybes <$> st.query.getManyTxBytes (nub txids)+  return [piece (return (listOf (map quoted found)))]+  where+    listOf items = "[" <> mconcat (intersperse "," items) <> "]"+    quoted bytes = "\"" <> rawHex bytes <> "\""  -- | The hashes of the transactions of a block. Responds with an error if -- not found.@@ -1529,9 +1538,9 @@ -- | Handler for @GET \/transactions\/block\/:block\/raw@: the transactions of a block, serialized, in pieces. handleTxsBlockRaw :: WebState -> GetTxsBlockRaw -> IO [Piece] handleTxsBlockRaw st (GetTxsBlockRaw h) =-  listPieces hex . transactionPieces st.query =<< getTxsBlock st h+  listPieces hex . pieced pieceSize (fmap catMaybes . st.query.getManyTxBytes) =<< getTxsBlock st h   where-    hex t = unsafeToEncoding ("\"" <> hexOf (transactionData t) <> "\"")+    hex bytes = unsafeToEncoding ("\"" <> rawHex bytes <> "\"")  -- GET TransactionAfterHeight -- @@ -3144,7 +3153,7 @@     [t] -> return $ Right t     [] -> return $ Left ThingNotFound     ts ->-      let tids = map (txHash . transactionData) ts+      let tids = map (.txid) ts        in return $ Left (TxIndexConflict tids)  -- | Handler for @GET \/blockchain\/rawtx\/:txid@: a transaction, as Blockchain.info.@@ -3164,8 +3173,12 @@       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+    -- the bytes as stored, of the transaction that the id names+    hx t =+      st.query.getManyTxBytes [t.txid] >>= \case+        [Just bytes] -> return (responseBuilder status200 textType (rawHex bytes))+        _ -> raise st ThingNotFound+    textType = [(hContentType, "text/plain; charset=utf-8")]  -- | Handler for @GET \/blockchain\/q\/txtotalbtcoutput\/:txid@: total value of the outputs of a transaction. handleBinfoTotalOut :: WebState -> Req -> IO Response
src/Haskoin/Store/Web/Pieces.hs view
@@ -90,10 +90,11 @@  import Control.Exception (evaluate) import Data.Aeson.Encoding (Encoding, fromEncoding)-import Data.ByteString.Builder (Builder, lazyByteString, lazyByteStringHex, toLazyByteString)+import Data.ByteString.Builder (Builder, lazyByteString, toLazyByteString) import Data.ByteString.Lazy qualified as L import Data.Bytes.Put (runPutL) import Data.Bytes.Serial (Serial (..))+import Haskoin.Util (hexBuilder) import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef) import Data.List (intersperse) import Data.Sequence (Seq (..), (|>))@@ -204,4 +205,4 @@  -- | The serialization of a value, in hexadecimal. hexOf :: (Serial a) => a -> Builder-hexOf = lazyByteStringHex . runPutL . serialize+hexOf = hexBuilder . runPutL . serialize
swagger.yaml view
@@ -11,7 +11,7 @@   - url: './'  info:-  version: &version 1.14.0+  version: &version 1.15.0   title: Haskoin   contact:     name: JP Rupp
+ test/Haskoin/Store/Database/SlicesSpec.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedRecordDot #-}++-- | The stored values read in part, held to the full decoders on random+-- values: a block whose transactions are decoded when used, and the bytes+-- of a stored transaction, which must be those that the raw answers sent+-- when they built the transaction and serialized it again.+module Haskoin.Store.Database.SlicesSpec (spec) where++import Data.ByteString qualified as B+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize)+import Data.IntMap.Strict qualified as IntMap+import Data.Serialize (encode)+import Haskoin+import Haskoin.Store.Data+import Haskoin.Store.Database.Slices (blockData, txBytes)+import Haskoin.Util.Arbitrary+import Test.Hspec+import Test.Hspec.QuickCheck (modifyMaxSuccess)+import Test.QuickCheck++spec :: Spec+spec = do+  ctx <- runIO createContext+  modifyMaxSuccess (const 500) $ describe "stored values read in part" $ do+    it "read a block as the full decoder does" $+      forAll arbitraryBlockData $ \b -> blockData (encode b) `shouldBe` Right b+    it "refuse a block that has fewer bytes than its count of transactions" $+      forAll arbitraryBlockData $ \b ->+        let bytes = encode b+            cut = B.take (B.length bytes - 25) bytes+         in not (null b.txs) ==> either (const True) (const False) (blockData cut)+    it "find the transaction of a stored one, and whether it is deleted" $+      forAll (arbitraryTxData ctx) $ \td ->+        txBytes (encode td) `shouldBe` Right (runPutS (serialize td.tx), td.deleted)+    it "give the bytes that building and serializing the transaction gave" $+      forAll (arbitraryTxData ctx) $ \td ->+        fmap fst (txBytes (encode td))+          `shouldBe` Right (runPutS (serialize (transactionData (toTransaction btc ctx td))))++arbitraryBlockData :: Gen BlockData+arbitraryBlockData =+  BlockData+    <$> arbitrary+    <*> arbitrary+    <*> oneof [choose (0, 2 ^ (31 :: Int)), choose (2 ^ (31 :: Int), 2 ^ (100 :: Int))]+    <*> arbitraryBlockHeader+    <*> arbitrary+    <*> arbitrary+    <*> oneof [listOf arbitraryTxHash, vectorOf 300 arbitraryTxHash]+    <*> arbitrary+    <*> arbitrary+    <*> arbitrary++arbitraryTxData :: Ctx -> Gen TxData+arbitraryTxData ctx =+  TxData+    <$> oneof [MemRef <$> arbitrary, BlockRef <$> arbitrary <*> arbitrary]+    <*> oneof [arbitraryTx btc ctx, arbitraryWitnessTx btc ctx, arbitraryLegacyTx btc ctx]+    <*> (IntMap.fromList <$> listOf ((,) <$> choose (0, 1000) <*> (Prev <$> arbitraryBS <*> arbitrary)))+    <*> arbitrary+    <*> arbitrary+    <*> arbitrary+    <*> (IntMap.fromList <$> listOf ((,) <$> choose (0, 1000) <*> (Spender <$> arbitraryTxHash <*> arbitrary)))
test/Haskoin/Store/IntegrationSpec.hs view
@@ -1156,6 +1156,10 @@   rawNode <- rpc env.node "getrawtransaction" [String t1, Bool False]   raw <- plainJSON env ("/transaction/" <> cs t1 <> "/raw")   field "result" raw `shouldBe` Just rawNode+  -- the same bytes as a list of one, and as the text of Blockchain.info+  plainJSON env ("/transactions/raw?txids=" <> cs t1) `shouldReturn` toJSON [rawNode]+  rawText <- Wreq.get (env.plain <> "/blockchain/rawtx/" <> cs t1 <> "?format=hex")+  String (cs (rawText ^. Wreq.responseBody)) `shouldBe` rawNode   bal <- plainJSON env ("/address/" <> cs a <> "/balance")   num "confirmed" bal `shouldBe` 400000   num "unconfirmed" bal `shouldBe` 50000