haskoin-core-1.5.0: src/Haskoin/Network/CompactBlock.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}
-- |
-- Module : Haskoin.Network.CompactBlock
-- Copyright : No rights reserved
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- Messages of the compact block relay protocol (BIP 152): @sendcmpct@,
-- @cmpctblock@, @getblocktxn@ and @blocktxn@.
module Haskoin.Network.CompactBlock
( SendCompact (..),
CompactBlock (..),
PrefilledTx (..),
GetBlockTxn (..),
BlockTxn (..),
maxShortId,
)
where
import Control.DeepSeq
import Control.Monad (when)
import Data.Binary (Binary (..))
import Data.Bits (shiftL, shiftR, (.|.))
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Serialize (Serialize (..))
import Data.Word (Word32, Word64)
import GHC.Generics (Generic)
import Haskoin.Block.Common (BlockHash, BlockHeader)
import Haskoin.Network.Common
import Haskoin.Transaction.Common (Tx)
-- | The @sendcmpct@ message. It tells the peer whether to announce new blocks
-- as compact blocks, and which version of the protocol to use. Version 1
-- identifies transactions by their txid and version 2 by their wtxid.
data SendCompact = SendCompact
{ -- | announce blocks with @cmpctblock@ instead of @inv@ or @headers@
announce :: !Bool,
-- | compact block protocol version
version :: !Word64
}
deriving (Eq, Show, Read, Generic, NFData)
instance Serial SendCompact where
deserialize = SendCompact <$> ((/= 0) <$> getWord8) <*> getWord64le
serialize (SendCompact a v) = do
putWord8 (if a then 1 else 0)
putWord64le v
instance Binary SendCompact where
put = serialize
get = deserialize
instance Serialize SendCompact where
put = serialize
get = deserialize
-- | Largest short transaction identifier. Short identifiers are six bytes.
maxShortId :: Word64
maxShortId = 0xffffffffffff
getShortId :: (MonadGet m) => m Word64
getShortId = do
low <- getWord32le
high <- getWord16le
return $ fromIntegral high `shiftL` 32 .|. fromIntegral low
putShortId :: (MonadPut m) => Word64 -> m ()
putShortId x
| x > maxShortId = error ("Short transaction id does not fit six bytes: " ++ show x)
| otherwise = do
putWord32le (fromIntegral x)
putWord16le (fromIntegral (x `shiftR` 32))
-- | A transaction sent along with a compact block, with its position in the
-- block.
data PrefilledTx = PrefilledTx
{ -- | position of the transaction in the block
index :: !Word32,
-- | the transaction
tx :: !Tx
}
deriving (Eq, Show, Generic, NFData)
-- | The @cmpctblock@ message: a block header with short identifiers for most
-- of its transactions, and the full transactions the sender expects the peer
-- to lack. Positions in the block are absolute here; on the wire each one is
-- encoded as its distance from the previous one, as BIP 152 requires.
data CompactBlock = CompactBlock
{ -- | block header
header :: !BlockHeader,
-- | nonce the short identifiers were computed with
nonce :: !Word64,
-- | short identifiers of the transactions not sent in full
shortIds :: ![Word64],
-- | transactions sent in full, in order of their position
prefilled :: ![PrefilledTx]
}
deriving (Eq, Show, Generic, NFData)
instance Serial CompactBlock where
deserialize =
CompactBlock
<$> deserialize
<*> getWord64le
<*> getVarIntList getShortId
<*> getDifferentialList (\i -> PrefilledTx i <$> deserialize)
serialize c = do
serialize c.header
putWord64le c.nonce
putVarIntList putShortId c.shortIds
putDifferentialList (.index) (serialize . (.tx)) c.prefilled
instance Binary CompactBlock where
put = serialize
get = deserialize
instance Serialize CompactBlock where
put = serialize
get = deserialize
-- | The @getblocktxn@ message. It asks for the transactions at the given
-- positions of a block. Positions are absolute here and differentially
-- encoded on the wire.
data GetBlockTxn = GetBlockTxn
{ -- | hash of the block
hash :: !BlockHash,
-- | positions of the wanted transactions, in increasing order
indexes :: ![Word32]
}
deriving (Eq, Show, Generic, NFData)
instance Serial GetBlockTxn where
deserialize = GetBlockTxn <$> deserialize <*> getDifferentialList return
serialize g = do
serialize g.hash
putDifferentialList id (const (return ())) g.indexes
instance Binary GetBlockTxn where
put = serialize
get = deserialize
instance Serialize GetBlockTxn where
put = serialize
get = deserialize
-- | The @blocktxn@ message, the answer to 'GetBlockTxn'.
data BlockTxn = BlockTxn
{ -- | hash of the block
hash :: !BlockHash,
-- | the requested transactions, in the order they were asked for
transactions :: ![Tx]
}
deriving (Eq, Show, Generic, NFData)
instance Serial BlockTxn where
deserialize = BlockTxn <$> deserialize <*> getVarIntList deserialize
serialize b = do
serialize b.hash
putVarIntList serialize b.transactions
instance Binary BlockTxn where
put = serialize
get = deserialize
instance Serialize BlockTxn where
put = serialize
get = deserialize
-- | Read a list whose elements each start with a block position. The first
-- position is written as is, and every later one as its distance from the
-- previous position minus one. A position that does not fit 32 bits fails
-- the parse.
getDifferentialList :: (MonadGet m) => (Word32 -> m a) -> m [a]
getDifferentialList element = do
VarInt count <- deserialize
when (count > maxListSize) $
fail ("List count too large: " ++ show count)
go count Nothing
where
go 0 _ = return []
go n previous = do
VarInt distance <- deserialize
let position = toInteger distance + maybe 0 (\p -> toInteger p + 1) previous
when (position > 0xffffffff) $
fail ("Block position does not fit 32 bits: " ++ show position)
let i = fromInteger position
x <- element i
(x :) <$> go (n - 1) (Just i)
-- | Write a list whose elements each carry a block position, in the encoding
-- that 'getDifferentialList' reads. Positions must be strictly increasing,
-- otherwise their distances cannot be encoded and this is an error.
putDifferentialList :: (MonadPut m) => (a -> Word32) -> (a -> m ()) -> [a] -> m ()
putDifferentialList position element xs = do
putVarInt (length xs)
go Nothing xs
where
go _ [] = return ()
go previous (x : rest) = do
let i = position x
case previous of
Nothing -> putVarInt i
Just p
| i > p -> putVarInt (i - p - 1)
| otherwise ->
error ("Block positions must be strictly increasing: " ++ show p ++ " then " ++ show i)
element x
go (Just i) rest