{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoFieldSelectors #-} -- | -- Module : Haskoin.Network.Bloom -- Copyright : No rights reserved -- License : MIT -- Maintainer : jprupp@protonmail.ch -- Stability : experimental -- Portability : POSIX -- -- Bloom filters are used to reduce data transfer when synchronizing thin cients. -- When bloom filters are used a client will obtain filtered blocks that only -- contain transactions that pass the bloom filter. Transactions announced via inv -- messages also pass the filter. module Haskoin.Network.Bloom ( -- * Bloom Filters BloomFlags (..), BloomFilter (..), FilterLoad (..), FilterAdd (..), bloomCreate, bloomInsert, bloomContains, isBloomValid, isBloomEmpty, isBloomFull, acceptsFilters, bloomRelevantUpdate, ) where import Control.DeepSeq import Control.Monad (forM_, replicateM) import Crypto.Secp256k1 (Ctx) import Data.Binary (Binary (..)) import Data.Bits import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.Bytes.Get import Data.Bytes.Put import Data.Bytes.Serial import Data.Foldable qualified as F import Data.Hash.Murmur (murmur3) import Data.List (foldl') import Data.Sequence qualified as S import Data.Serialize (Serialize (..)) import Data.Word import GHC.Generics (Generic) import Haskoin.Network.Common import Haskoin.Script.Standard import Haskoin.Transaction.Common import Haskoin.Util.Marshal -- | 20,000 items with fp rate < 0.1% or 10,000 items and <0.0001% maxBloomSize :: Int maxBloomSize = 36000 maxHashFuncs :: Word32 maxHashFuncs = 50 ln2Squared :: Double ln2Squared = 0.4804530139182014246671025263266649717305529515945455 ln2 :: Double ln2 = 0.6931471805599453094172321214581765680755001343602552 bitMask :: [Word8] bitMask = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] -- | The bloom flags are used to tell the remote peer how to auto-update -- the provided bloom filter. data BloomFlags = -- | never update BloomUpdateNone | -- | auto-update on all outputs BloomUpdateAll | -- | auto-update on pay-to-pubkey or pay-to-multisig (default) BloomUpdateP2PubKeyOnly deriving (Eq, Show, Read, Generic, NFData) instance Serial BloomFlags where deserialize = go =<< getWord8 where go 0 = return BloomUpdateNone go 1 = return BloomUpdateAll go 2 = return BloomUpdateP2PubKeyOnly go _ = fail "BloomFlags get: Invalid bloom flag" serialize f = putWord8 $ case f of BloomUpdateNone -> 0 BloomUpdateAll -> 1 BloomUpdateP2PubKeyOnly -> 2 instance Binary BloomFlags where get = deserialize put = serialize instance Serialize BloomFlags where get = deserialize put = serialize -- | A bloom filter is a probabilistic data structure that SPV clients send to -- other peers to filter the set of transactions received from them. Bloom -- filters can have false positives but not false negatives. Some transactions -- that pass the filter may not be relevant to the receiving peer. By -- controlling the false positive rate, SPV nodes can trade off bandwidth -- versus privacy. data BloomFilter = BloomFilter { -- | bloom filter data array :: !(S.Seq Word8), -- | number of hash functions for this filter functions :: !Word32, -- | hash function random nonce tweak :: !Word32, -- | bloom filter auto-update flags flags :: !BloomFlags } deriving (Eq, Show, Read, Generic, NFData) instance Serial BloomFilter where deserialize = BloomFilter <$> (S.fromList <$> (readDat =<< deserialize)) <*> getWord32le <*> getWord32le <*> deserialize where readDat (VarInt len) = replicateM (fromIntegral len) getWord8 serialize BloomFilter {..} = do putVarInt $ S.length array mapM_ putWord8 (F.toList array) putWord32le functions putWord32le tweak serialize flags instance Binary BloomFilter where put = serialize get = deserialize instance Serialize BloomFilter where put = serialize get = deserialize -- | Set a new bloom filter on the peer connection. newtype FilterLoad = FilterLoad {filter :: BloomFilter} deriving (Eq, Show, Read, Generic) deriving newtype (NFData) instance Serial FilterLoad where deserialize = FilterLoad <$> deserialize serialize (FilterLoad f) = serialize f instance Binary FilterLoad where put = serialize get = deserialize instance Serialize FilterLoad where put = serialize get = deserialize -- | Add the given data element to the connections current filter without -- requiring a completely new one to be set. newtype FilterAdd = FilterAdd {get :: ByteString} deriving (Eq, Show, Read, Generic) deriving newtype (NFData) instance Serial FilterAdd where deserialize = do (VarInt len) <- deserialize dat <- getByteString $ fromIntegral len return $ FilterAdd dat serialize (FilterAdd bs) = do putVarInt $ BS.length bs putByteString bs instance Binary FilterAdd where put = serialize get = deserialize instance Serialize FilterAdd where put = serialize get = deserialize -- | Build a bloom filter that will provide the given false positive rate when -- the given number of elements have been inserted. bloomCreate :: -- | number of elements Int -> -- | false positive rate Double -> -- | random nonce (tweak) for the hash function Word32 -> -- | bloom filter flags BloomFlags -> -- | bloom filter BloomFilter bloomCreate numElem fpRate = BloomFilter (S.replicate bloomSize 0) numHashF where -- Bloom filter size in bytes bloomSize = truncate $ min a b / 8 -- Suggested size in bits a = -1 / ln2Squared * fromIntegral numElem * log fpRate -- Maximum size in bits b = fromIntegral $ maxBloomSize * 8 numHashF = truncate $ min c (fromIntegral maxHashFuncs) -- Suggested number of hash functions c = fromIntegral bloomSize * 8 / fromIntegral numElem * ln2 bloomHash :: BloomFilter -> Word32 -> ByteString -> Word32 bloomHash b hashNum bs = murmur3 seed bs `mod` (fromIntegral (S.length (b.array)) * 8) where seed = hashNum * 0xfba4c795 + b.tweak -- | Insert arbitrary data into a bloom filter. Returns the new bloom filter -- containing the new data. bloomInsert :: -- | Original bloom filter BloomFilter -> -- | New data to insert ByteString -> -- | Bloom filter containing the new data BloomFilter bloomInsert b bs | isBloomFull b = b | otherwise = b {array = dat} where idxs = map (\i -> bloomHash b i bs) [0 .. b.functions - 1] upd s i = S.adjust (.|. bitMask !! fromIntegral (7 .&. i)) (fromIntegral $ i `shiftR` 3) s dat = foldl upd b.array idxs -- | Tests if some arbitrary data matches the filter. This can be either because -- the data was inserted into the filter or because it is a false positive. bloomContains :: -- | Bloom filter BloomFilter -> -- | Data that will be checked against the given bloom filter ByteString -> -- | Returns True if the data matches the filter Bool bloomContains b bs | isBloomFull b = True | isBloomEmpty b = False | otherwise = all isSet idxs where s = b.array idxs = map (\i -> bloomHash b i bs) [0 .. b.functions - 1] isSet i = S.index s (fromIntegral $ i `shiftR` 3) .&. (bitMask !! fromIntegral (7 .&. i)) /= 0 -- | Checks if any of the outputs of a tx is in the current bloom filter. -- If it is, add the txid and vout as an outpoint (i.e. so that -- a future tx that spends the output won't be missed). bloomRelevantUpdate :: Ctx -> -- | Bloom filter BloomFilter -> -- | Tx that may (or may not) have relevant outputs Tx -> -- | Returns an updated bloom filter adding relevant output Maybe BloomFilter bloomRelevantUpdate ctx b tx | isBloomFull b || isBloomEmpty b = Nothing | b.flags == BloomUpdateNone = Nothing | not (null matchOuts) = Just $ foldl' addRelevant b matchOuts | otherwise = Nothing where -- TxHash if we end up inserting an outpoint h = txHash tx -- Decode the scriptOutpus and add vOuts in case we make them outpoints decodedOutputScripts = traverse (unmarshal ctx . (.script)) tx.outputs err = error "Error Decoding output script" idxOutputScripts = either (const err) (zip [0 ..]) decodedOutputScripts -- Check if any txOuts were contained in the bloom filter matchFilter = filter (\(_, op) -> any (bloomContains b) (encodeScriptOut op)) matchOuts = matchFilter idxOutputScripts addRelevant :: BloomFilter -> (Word32, ScriptOutput) -> BloomFilter addRelevant bf (id', scriptOut) = case (b.flags, scriptType) of -- We filtered out BloomUpdateNone so we insert any PayPk or PayMulSig (_, True) -> bloomInsert bf outpoint (BloomUpdateAll, _) -> bloomInsert bf outpoint _ -> error "Error Updating Bloom Filter with relevant outpoint" where outpoint = runPutS $ serialize $ OutPoint {hash = h, index = id'} scriptType = (\s -> isPayPK s || isPayMulSig s) scriptOut -- Encodes a scriptOutput so it can be checked agains the Bloom Filter encodeScriptOut :: ScriptOutput -> [ByteString] encodeScriptOut (PayPK pk) = return $ marshal ctx pk encodeScriptOut (PayPKHash ph) = return . runPutS $ serialize ph encodeScriptOut (PayMulSig outputMuSig _) = map (marshal ctx) outputMuSig encodeScriptOut (PayScriptHash sh) = return . runPutS $ serialize sh encodeScriptOut (PayWitnessPKHash ph) = return . runPutS $ serialize ph encodeScriptOut (PayWitnessScriptHash sh) = return . runPutS $ serialize sh encodeScriptOut (PayWitness _ wd) = return wd encodeScriptOut (DataCarrier dat) = return dat -- | Returns True if the filter is empty (all bytes set to 0x00) isBloomEmpty :: BloomFilter -> Bool isBloomEmpty b = all (== 0x00) $ F.toList b.array -- | Returns True if the filter is full (all bytes set to 0xff) isBloomFull :: BloomFilter -> Bool isBloomFull b = all (== 0xff) $ F.toList b.array -- | Tests if a given bloom filter is valid. isBloomValid :: -- | Bloom filter to test BloomFilter -> -- | True if the given filter is valid Bool isBloomValid BloomFilter {..} = S.length array <= maxBloomSize && functions <= maxHashFuncs -- | Does the peer with these version services accept bloom filters? acceptsFilters :: Word64 -> Bool acceptsFilters srv = srv .&. (1 `shiftL` 2) /= 0