haskoin-core-1.6.0: src/Haskoin/Network/CompactBlock.hs
{-# LANGUAGE BangPatterns #-}
{-# 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,
getCompactBlock,
getGetBlockTxn,
getBlockTxn,
)
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.Network.Data (Network (..), getNetworkTx)
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)
-- | This form knows no network, so it takes positions of up to 32 bits and
-- reads transactions in the extended format. Use 'getCompactBlock' for a
-- message from a peer.
instance Serial CompactBlock where
deserialize = compactBlockWith maxBound 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)
-- | This form knows no network, so it takes positions of up to 32 bits. Use
-- 'getGetBlockTxn' for a message from a peer.
instance Serial GetBlockTxn where
deserialize = GetBlockTxn <$> deserialize <*> getDifferentialList maxBound 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 @cmpctblock@ message of a network. On Bitcoin a position above
-- 65535 fails the parse, and so does a block of more than 65535
-- transactions, as in Bitcoin Core ("indexes overflowed 16 bits"). On
-- Bitcoin Cash positions have 32 bits, as in Bitcoin Cash Node, and the
-- transactions are read in the old format.
getCompactBlock :: (MonadGet m) => Network -> m CompactBlock
getCompactBlock net = compactBlockWith net.maxCompactBlockIndex (getNetworkTx net)
compactBlockWith :: (MonadGet m) => Word32 -> m Tx -> m CompactBlock
compactBlockWith maxIndex getTx = do
header <- deserialize
nonce <- getWord64le
shortIds <- getVarIntList getShortId
prefilled <- getDifferentialList maxIndex (\i -> PrefilledTx i <$> getTx)
-- The positions of a block of n transactions are 0 to n - 1, and Bitcoin
-- Core refuses a count above the largest value of its position type.
when (toInteger (length shortIds) + toInteger (length prefilled) > toInteger maxIndex) $
fail ("Compact block: indexes overflowed " ++ show (indexBits maxIndex) ++ " bits")
return (CompactBlock header nonce shortIds prefilled)
-- | Read a @getblocktxn@ message of a network. On Bitcoin a position above
-- 65535 fails the parse, as in Bitcoin Core ("differential value overflow").
getGetBlockTxn :: (MonadGet m) => Network -> m GetBlockTxn
getGetBlockTxn net =
GetBlockTxn <$> deserialize <*> getDifferentialList net.maxCompactBlockIndex return
-- | Read a @blocktxn@ message of a network. On a network without SegWit the
-- transactions are read in the old format, see 'getNetworkTx'.
getBlockTxn :: (MonadGet m) => Network -> m BlockTxn
getBlockTxn net = BlockTxn <$> deserialize <*> getVarIntList (getNetworkTx net)
-- | Width of the position type that holds this largest position.
indexBits :: Word32 -> Int
indexBits maxIndex
| maxIndex <= 0xffff = 16
| otherwise = 32
-- | 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 above the given largest one fails
-- the parse. Positions are strict numbers, so a long list costs no more than
-- its cells.
getDifferentialList :: (MonadGet m) => Word32 -> (Word32 -> m a) -> m [a]
getDifferentialList maxIndex element = do
VarInt count <- deserialize
when (count > maxListSize) $
fail ("List count too large: " ++ show count)
-- Positions increase strictly, so there are at most maxIndex + 1 of them.
when (count > limit + 1) $ tooFar count
go count Nothing []
where
limit = fromIntegral maxIndex :: Word64
tooFar x =
fail ("Block position does not fit " ++ show (indexBits maxIndex) ++ " bits: " ++ show x)
go 0 _ acc = return (reverse acc)
go n previous acc = do
VarInt distance <- deserialize
-- Checked first, so that the sum below cannot wrap around.
when (distance > limit) $ tooFar distance
let position = distance + maybe 0 (\p -> fromIntegral p + 1) previous
when (position > limit) $ tooFar position
let !i = fromIntegral position :: Word32
x <- element i
go (n - 1) (Just i) (x : acc)
-- | 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