haskoin-store-1.15.0: src/Haskoin/Store/Database/Slices.hs
{-# 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)