packages feed

bitcoin-compact-filters-0.1.1: src/Bitcoin/CompactFilter.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}

module Bitcoin.CompactFilter (
    BlockFilter,
    blockFilter,
    BlockFilterHeader,
    blockFilterHeader,
    filterHeaderToHex,
    filterHeaderFromHex,
    genesisHeader,
    filterContents,
    encodeFilter,
    isMember,
) where

import Control.Monad (replicateM, (>=>))
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State.Strict (StateT, evalStateT)
import qualified Control.Monad.Trans.State.Strict as St
import Data.Bits (clearBit, setBit, shiftL, shiftR, testBit)
import Data.Bool (bool)
import Data.ByteArray.Hash (
    SipHash (..),
    SipKey (..),
    sipHash,
 )
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Foldable (foldl')
import Data.List (sort)
import Data.Serialize (
    Get,
    Serialize (..),
    decode,
    encode,
    getWord16le,
    getWord32le,
    getWord64le,
    getWord8,
    putByteString,
    putWord16le,
    putWord32le,
    putWord64le,
    putWord8,
    runGet,
 )
import Data.Text (Text)
import Data.Word (Word64, Word8)
import Haskoin.Block (
    Block,
    BlockHash,
    blockHeader,
    blockTxns,
    headerHash,
 )
import Haskoin.Crypto (Hash256, doubleSHA256)
import Haskoin.Script (Script (..), ScriptOp (..))
import Haskoin.Transaction (scriptOutput, txOut)
import Haskoin.Util (decodeHex, encodeHex)

-- | SIP hash parameter
paramP :: Int
paramP = 19

-- | SIP hash parameter
paramM :: Word64
paramM = 784931

-- | Hashes of scripts in the block
newtype BlockFilter = BlockFilter
    { blockFilter :: [Word64]
    -- ^ Get the list of hashes in increasing order
    }
    deriving (Eq, Show)

-- | Number of elements in the filter
blockFilterSize :: BlockFilter -> CompactSize
blockFilterSize = CompactSize . length . blockFilter

newtype BlockFilterHeader = BlockFilterHeader {getBFHeader :: Hash256}
    deriving (Eq, Show, Ord, Serialize)

filterHeaderToHex :: BlockFilterHeader -> Text
filterHeaderToHex = encodeHex . BS.reverse . encode . getBFHeader

filterHeaderFromHex :: Text -> Either String BlockFilterHeader
filterHeaderFromHex = maybe (Left "Invalid hex") Right . decodeHex >=> decode . BS.reverse

genesisHeader :: BlockFilterHeader
Right genesisHeader = BlockFilterHeader <$> decode (BS.replicate 32 0x0)

-- | Calculate the header for the block filter
blockFilterHeader ::
    -- | previous header
    BlockFilterHeader ->
    -- | current filter
    BlockFilter ->
    BlockFilterHeader
blockFilterHeader prev bf =
    BlockFilterHeader . doubleSHA256 $
        (encode . doubleSHA256) (encode bf) <> encode (getBFHeader prev)

instance Serialize BlockFilter where
    put bf = put (blockFilterSize bf) >> putByteString (constructGCS paramP $ blockFilter bf)
    get = get >>= fmap BlockFilter . getGolombRiceSet paramP . unCompactSize

-- | Calculate the list of scripts which belong in the BIP158 block filter
filterContents ::
    -- | previous output scripts spent in this block
    [ByteString] ->
    Block ->
    [ByteString]
filterContents prev b = filter scriptFilter prev <> these
  where
    these = filter scriptFilter . fmap scriptOutput $ blockTxns b >>= txOut
    scriptFilter scr = not (BS.null scr) && contentFilter scr

    contentFilter bs = case decode bs of
        Right (Script (OP_RETURN : _)) -> False
        _ -> True

-- | Construct a BIP158 filter from a block
encodeFilter ::
    -- | output scripts spent in this block
    [ByteString] ->
    Block ->
    BlockFilter
encodeFilter os b = BlockFilter s
  where
    h = headerHash $ blockHeader b
    bs = toSet $ filterContents os b
    s = hashedSetConstruct (sipKey h) paramM (length bs) bs

-- | Test membership.  The test succeeds if /any/ of the scripts matches the block filter.
isMember ::
    BlockHash ->
    -- | Scripts we want to match against the filter
    [ByteString] ->
    BlockFilter ->
    Bool
isMember h bs (BlockFilter bf) = orderedScan hs bf
  where
    k = sipKey h
    hs = hashedSetConstruct k paramM (length bf) bs

orderedScan :: [Word64] -> [Word64] -> Bool
orderedScan xs@(x : xs') hs@(h : hs')
    | x > h = orderedScan xs hs'
    | x < h = orderedScan xs' hs
    | otherwise = True
orderedScan _ _ = False

type GetBits = StateT [Bool] Get

getGolombRiceSet :: Int -> Int -> Get [Word64]
getGolombRiceSet p n = fmap unDiffs . evalBitstream $ replicateM n getEncoded
  where
    getEncoded = do
        q <- unaryPart
        r <- fromBits <$> getBits p
        return $ q * 2 ^ p + r

unaryPart :: GetBits Word64
unaryPart = go 0
  where
    go q = getBit >>= bool (return q) (go $ q + 1)
    getBit = head <$> getBits 1

evalBitstream :: GetBits a -> Get a
evalBitstream = (`evalStateT` mempty)

getBits :: Int -> GetBits [Bool]
getBits n = do
    bs <- St.get
    let l = length bs
        (q, r) = (n - l) `quotRem` 8

        combine m t = bs <> mconcat m <> t

        lastByte
            | r > 0 = getBs >>= takeSome r
            | otherwise = mempty <$ St.put mempty

        getBs = byteBits <$> lift getWord8
        takeSome m xs = take m xs <$ St.put (drop m xs)

    if n < l
        then takeSome n bs
        else combine <$> replicateM q getBs <*> lastByte

sipKey :: BlockHash -> SipKey
sipKey h = SipKey k1 k2
  where
    Right (k1, k2) = runGet word64Pair $ encode h
    word64Pair = (,) <$> getWord64le <*> getWord64le

hashToRange :: Word64 -> SipKey -> ByteString -> Word64
hashToRange f k bs = v
  where
    SipHash h = sipHash k bs
    v = remap (fromIntegral f) (fromIntegral h)

    remap :: Integer -> Integer -> Word64
    remap x y = fromIntegral $ (x * y) `shiftR` 64

hashedSetConstruct :: SipKey -> Word64 -> Int -> [ByteString] -> [Word64]
hashedSetConstruct k m n bs = toSet $ hashToRange f k <$> bs
  where
    f = fromIntegral n * m

toSet :: Ord a => [a] -> [a]
toSet = dedup . sort
  where
    dedup = \case
        (x0 : xs@(x1 : _))
            | x0 == x1 -> dedup xs
            | otherwise -> x0 : dedup xs
        xs -> xs

{- | Golomb coded sets are not naturally expressed in bytes, but rather as a bit
 stream
-}
data Bitstream
    = Bitstream
        BSL.ByteString
        -- ^ Complete bytes written so far, in reverse order
        {-# UNPACK #-} !Word8
        -- ^ The current work byte
        {-# UNPACK #-} !Int
        -- ^ Pointer to the first open bit

emptyB :: Bitstream
emptyB = Bitstream mempty 0 7

appendBit :: Bool -> Bitstream -> Bitstream
appendBit b (Bitstream bytes inFlight cursor)
    | cursor == 0 = Bitstream (BSL.cons nextInFlight bytes) 0 7
    | otherwise = Bitstream bytes nextInFlight (cursor - 1)
  where
    nextInFlight = bool clearBit setBit b inFlight cursor

asByteString :: Bitstream -> ByteString
asByteString (Bitstream bytes inFlight cursor) =
    BSL.toStrict $ BSL.reverse paddedBytes
  where
    paddedBytes
        | cursor == 7 = bytes
        | otherwise = BSL.cons inFlight bytes

constructGCS ::
    -- | modulus
    Int ->
    -- | sorted list of input values
    [Word64] ->
    ByteString
constructGCS p =
    asByteString
        . foldl' (golombRiceEncode p) emptyB
        . diffs

diffs :: Num a => [a] -> [a]
diffs xs = zipWith (-) xs (0 : xs)

unDiffs :: Num a => [a] -> [a]
unDiffs (x : xs) = scanl (+) x xs
unDiffs [] = []

golombRiceEncode :: Int -> Bitstream -> Word64 -> Bitstream
golombRiceEncode p b v = foldl' (flip nextBit) prefix [p - i | i <- [1 .. p]]
  where
    q = fromIntegral $ v `shiftR` p
    prefix = appendBit False $ iterate (appendBit True) b !! q
    nextBit = appendBit . testBit v

fromBits :: Num a => [Bool] -> a
fromBits = foldl' onBit 0
  where
    onBit n b = 2 * n + bool 0 1 b

byteBits :: Word8 -> [Bool]
byteBits b = testBit b <$> reverse [0 .. 7]

newtype CompactSize = CompactSize {unCompactSize :: Int} deriving (Eq, Ord, Enum, Num, Real, Integral)

instance Serialize CompactSize where
    get = CompactSize <$> (getWord8 >>= getCS)
      where
        getCS s
            | s < 253 = return $ fromIntegral s
            | s == 253 = fromIntegral <$> getWord16le
            | s == 254 = fromIntegral <$> getWord32le
            | otherwise = fromIntegral <$> getWord64le

    put (CompactSize n)
        | n < 0 = error $ "Invalid CompactSize: " <> show n
        | n < 253 = putWord8 (fromIntegral n)
        | n < bound16 = putWord8 253 >> putWord16le (fromIntegral n)
        | n < bound32 = putWord8 254 >> putWord32le (fromIntegral n)
        | otherwise = putWord8 255 >> putWord64le (fromIntegral n)
      where
        bound16 = 1 `shiftL` 16
        bound32 = 1 `shiftL` 32