haskoin-core-0.13.2: src/Haskoin/Network/Common.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-|
Module : Haskoin.Network.Common
Copyright : No rights reserved
License : UNLICENSE
Maintainer : jprupp@protonmail.ch
Stability : experimental
Portability : POSIX
Common functions and data types related to peer-to-peer network.
-}
module Haskoin.Network.Common
( -- * Network Data Types
Addr(..)
, NetworkAddressTime
, Alert(..)
, GetData(..)
, Inv(..)
, InvVector(..)
, InvType(..)
, HostAddress
, hostToSockAddr
, sockToHostAddress
, NetworkAddress(..)
, NotFound(..)
, Ping(..)
, Pong(..)
, Reject(..)
, RejectCode(..)
, VarInt(..)
, VarString(..)
, Version(..)
, MessageCommand(..)
-- ** Useful Functions
, reject
, nodeNone
, nodeNetwork
, nodeGetUTXO
, nodeBloom
, nodeWitness
, nodeXThin
, commandToString
, stringToCommand
, putVarInt
) where
import Control.DeepSeq
import Control.Monad (forM_, liftM2, replicateM, unless)
import Data.Bits (shiftL)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Char8 as C (replicate)
import Data.Serialize as S
import Data.String
import Data.String.Conversions (cs)
import Data.Word (Word32, Word64)
import GHC.Generics (Generic)
import Haskoin.Crypto.Hash
import Network.Socket (SockAddr (..))
import Text.Read as R
-- | Network address with a timestamp.
type NetworkAddressTime = (Word32, NetworkAddress)
-- | Provides information about known nodes in the bitcoin network. An 'Addr'
-- type is sent inside a 'Message' as a response to a 'GetAddr' message.
newtype Addr =
Addr { -- List of addresses of other nodes on the network with timestamps.
addrList :: [NetworkAddressTime]
}
deriving (Eq, Show, Generic, NFData)
instance Serialize Addr where
get = Addr <$> (repList =<< S.get)
where
repList (VarInt c) = replicateM (fromIntegral c) action
action = liftM2 (,) getWord32le S.get
put (Addr xs) = do
putVarInt $ length xs
forM_ xs $ \(a,b) -> putWord32le a >> put b
-- | Data type describing signed messages that can be sent between bitcoin
-- nodes to display important notifications to end users about the health of
-- the network.
data Alert =
Alert {
-- | Alert payload.
alertPayload :: !VarString
-- | ECDSA signature of the payload
, alertSignature :: !VarString
} deriving (Eq, Show, Read, Generic, NFData)
instance Serialize Alert where
get = Alert <$> S.get <*> S.get
put (Alert p s) = put p >> put s
-- | The 'GetData' type is used to retrieve information on a specific object
-- ('Block' or 'Tx') identified by the objects hash. The payload of a 'GetData'
-- request is a list of 'InvVector' which represent all the hashes of objects
-- that a node wants. The response to a 'GetBlock' message will be either a
-- 'Block' or a 'Tx' message depending on the type of the object referenced by
-- the hash. Usually, 'GetData' messages are sent after a node receives an 'Inv'
-- message that contains unknown object hashes.
newtype GetData =
GetData { -- | list of object hashes
getDataList :: [InvVector]
} deriving (Eq, Show, Generic, NFData)
instance Serialize GetData where
get = GetData <$> (repList =<< S.get)
where
repList (VarInt c) = replicateM (fromIntegral c) S.get
put (GetData xs) = do
putVarInt $ length xs
forM_ xs put
-- | 'Inv' messages are used by nodes to advertise their knowledge of new
-- objects by publishing a list of hashes to a peer. 'Inv' messages can be sent
-- unsolicited or in response to a 'GetBlocks' message.
newtype Inv =
Inv {
-- | inventory
invList :: [InvVector]
} deriving (Eq, Show, Generic, NFData)
instance Serialize Inv where
get = Inv <$> (repList =<< S.get)
where
repList (VarInt c) = replicateM (fromIntegral c) S.get
put (Inv xs) = do
putVarInt $ length xs
forM_ xs put
-- | Data type identifying the type of an inventory vector. SegWit types are
-- only used in 'GetData' messages, not 'Inv'.
data InvType
= InvError -- ^ error
| InvTx -- ^ transaction
| InvBlock -- ^ block
| InvMerkleBlock -- ^ filtered block
| InvWitnessTx -- ^ segwit transaction
| InvWitnessBlock -- ^ segwit block
| InvWitnessMerkleBlock -- ^ segwit filtere block
deriving (Eq, Show, Read, Generic, NFData)
instance Serialize InvType where
get = go =<< getWord32le
where
go x =
case x of
0 -> return InvError
1 -> return InvTx
2 -> return InvBlock
3 -> return InvMerkleBlock
_
| x == 1 `shiftL` 30 + 1 -> return InvWitnessTx
| x == 1 `shiftL` 30 + 2 -> return InvWitnessBlock
| x == 1 `shiftL` 30 + 3 -> return InvWitnessMerkleBlock
| otherwise -> fail "bitcoinGet InvType: Invalid Type"
put x =
putWord32le $
case x of
InvError -> 0
InvTx -> 1
InvBlock -> 2
InvMerkleBlock -> 3
InvWitnessTx -> 1 `shiftL` 30 + 1
InvWitnessBlock -> 1 `shiftL` 30 + 2
InvWitnessMerkleBlock -> 1 `shiftL` 30 + 3
-- | Invectory vectors represent hashes identifying objects such as a 'Block' or
-- a 'Tx'. They notify other peers about new data or data they have otherwise
-- requested.
data InvVector =
InvVector {
-- | type of object
invType :: !InvType
-- | 256-bit hash of object
, invHash :: !Hash256
} deriving (Eq, Show, Generic, NFData)
instance Serialize InvVector where
get = InvVector <$> S.get <*> S.get
put (InvVector t h) = put t >> put h
newtype HostAddress =
HostAddress ByteString
deriving (Eq, Show, Ord, Generic, NFData)
instance Serialize HostAddress where
put (HostAddress bs) = putByteString bs
get = HostAddress <$> getByteString 18
-- | Data type describing a bitcoin network address. Addresses are stored in
-- IPv6 format. IPv4 addresses are mapped to IPv6 using IPv4 mapped IPv6
-- addresses: <http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses>.
data NetworkAddress =
NetworkAddress { -- | bitmask of services available for this address
naServices :: !Word64
-- | address and port information
, naAddress :: !HostAddress
} deriving (Eq, Show, Generic, NFData)
hostToSockAddr :: HostAddress -> SockAddr
hostToSockAddr (HostAddress bs) =
case runGet getSockAddr bs of
Left e -> error e
Right x -> x
sockToHostAddress :: SockAddr -> HostAddress
sockToHostAddress = HostAddress . runPut . putSockAddr
putSockAddr :: SockAddr -> Put
putSockAddr (SockAddrInet6 p _ (a, b, c, d) _) = do
putWord32be a
putWord32be b
putWord32be c
putWord32be d
putWord16be (fromIntegral p)
putSockAddr (SockAddrInet p a) = do
putWord32be 0x00000000
putWord32be 0x00000000
putWord32be 0x0000ffff
putWord32host a
putWord16be (fromIntegral p)
putSockAddr _ = error "Invalid address type"
getSockAddr :: Get SockAddr
getSockAddr = do
a <- getWord32be
b <- getWord32be
c <- getWord32be
if a == 0x00000000 && b == 0x00000000 && c == 0x0000ffff
then do
d <- getWord32host
p <- getWord16be
return $ SockAddrInet (fromIntegral p) d
else do
d <- getWord32be
p <- getWord16be
return $ SockAddrInet6 (fromIntegral p) 0 (a, b, c, d) 0
instance Serialize NetworkAddress where
get = NetworkAddress <$> getWord64le <*> S.get
put (NetworkAddress s a) = putWord64le s >> put a
-- | A 'NotFound' message is returned as a response to a 'GetData' message
-- whe one of the requested objects could not be retrieved. This could happen,
-- for example, if a tranasaction was requested and was not available in the
-- memory pool of the receiving node.
newtype NotFound =
NotFound { -- | Inventory vectors related to this request
notFoundList :: [InvVector]
} deriving (Eq, Show, Generic, NFData)
instance Serialize NotFound where
get = NotFound <$> (repList =<< S.get)
where
repList (VarInt c) = replicateM (fromIntegral c) S.get
put (NotFound xs) = do
putVarInt $ length xs
forM_ xs put
-- | A 'Ping' message is sent to bitcoin peers to check if a connection is still
-- open.
newtype Ping =
Ping { -- | A random nonce used to identify the recipient of the ping
-- request once a Pong response is received.
pingNonce :: Word64
} deriving (Eq, Show, Read, Generic, NFData)
-- | A Pong message is sent as a response to a ping message.
newtype Pong =
Pong {
-- | nonce from corresponding 'Ping'
pongNonce :: Word64
} deriving (Eq, Show, Read, Generic, NFData)
instance Serialize Ping where
get = Ping <$> getWord64le
put (Ping n) = putWord64le n
instance Serialize Pong where
get = Pong <$> getWord64le
put (Pong n) = putWord64le n
-- | The 'Reject' message is sent when messages are rejected by a peer.
data Reject =
Reject {
-- | type of message rejected
rejectMessage :: !MessageCommand
-- | rejection code
, rejectCode :: !RejectCode
-- | text reason for rejection
, rejectReason :: !VarString
-- | extra data such as block or tx hash
, rejectData :: !ByteString
} deriving (Eq, Show, Read, Generic, NFData)
-- | Rejection code associated to the 'Reject' message.
data RejectCode
= RejectMalformed
| RejectInvalid
| RejectObsolete
| RejectDuplicate
| RejectNonStandard
| RejectDust
| RejectInsufficientFee
| RejectCheckpoint
deriving (Eq, Show, Read, Generic, NFData)
instance Serialize RejectCode where
get = getWord8 >>= \code -> case code of
0x01 -> return RejectMalformed
0x10 -> return RejectInvalid
0x11 -> return RejectObsolete
0x12 -> return RejectDuplicate
0x40 -> return RejectNonStandard
0x41 -> return RejectDust
0x42 -> return RejectInsufficientFee
0x43 -> return RejectCheckpoint
_ -> fail $ unwords
[ "Reject get: Invalid code"
, show code
]
put code = putWord8 $ case code of
RejectMalformed -> 0x01
RejectInvalid -> 0x10
RejectObsolete -> 0x11
RejectDuplicate -> 0x12
RejectNonStandard -> 0x40
RejectDust -> 0x41
RejectInsufficientFee -> 0x42
RejectCheckpoint -> 0x43
-- | Convenience function to build a 'Reject' message.
reject :: MessageCommand -> RejectCode -> ByteString -> Reject
reject cmd code reason =
Reject cmd code (VarString reason) B.empty
instance Serialize Reject where
get =
S.get >>= \(VarString bs) ->
Reject (stringToCommand bs) <$> S.get <*> S.get <*> maybeData
where
maybeData =
isEmpty >>= \done ->
if done
then return B.empty
else getByteString 32
put (Reject cmd code reason dat) = do
put $ VarString $ commandToString cmd
put code
put reason
unless (B.null dat) $ putByteString dat
-- | Data type representing a variable-length integer. The 'VarInt' type
-- usually precedes an array or a string that can vary in length.
newtype VarInt = VarInt { getVarInt :: Word64 }
deriving (Eq, Show, Read, Generic, NFData)
instance Serialize VarInt where
get = VarInt <$> ( getWord8 >>= go )
where
go 0xff = getWord64le
go 0xfe = fromIntegral <$> getWord32le
go 0xfd = fromIntegral <$> getWord16le
go x = fromIntegral <$> return x
put (VarInt x)
| x < 0xfd =
putWord8 $ fromIntegral x
| x <= 0xffff = do
putWord8 0xfd
putWord16le $ fromIntegral x
| x <= 0xffffffff = do
putWord8 0xfe
putWord32le $ fromIntegral x
| otherwise = do
putWord8 0xff
putWord64le x
putVarInt :: Integral a => a -> Put
putVarInt = put . VarInt . fromIntegral
-- | Data type for serialization of variable-length strings.
newtype VarString = VarString { getVarString :: ByteString }
deriving (Eq, Show, Read, Generic, NFData)
instance Serialize VarString where
get = VarString <$> (readBS =<< S.get)
where
readBS (VarInt len) = getByteString (fromIntegral len)
put (VarString bs) = do
putVarInt $ B.length bs
putByteString bs
-- | When a bitcoin node creates an outgoing connection to another node,
-- the first message it will send is a 'Version' message. The other node
-- will similarly respond with it's own 'Version' message.
data Version =
Version {
-- | protocol version
version :: !Word32
-- | features supported by this connection
, services :: !Word64
-- | unix timestamp
, timestamp :: !Word64
-- | network address of remote node
, addrRecv :: !NetworkAddress
-- | network address of sending node
, addrSend :: !NetworkAddress
-- | random nonce to detect connection to self
, verNonce :: !Word64
-- | user agent string
, userAgent :: !VarString
-- | height of the last block in sending node
, startHeight :: !Word32
-- | relay transactions flag (BIP-37)
, relay :: !Bool
} deriving (Eq, Show, Generic, NFData)
instance Serialize Version where
get = Version <$> getWord32le
<*> getWord64le
<*> getWord64le
<*> S.get
<*> S.get
<*> getWord64le
<*> S.get
<*> getWord32le
<*> (go =<< isEmpty)
where
go True = return True
go False = getBool
put (Version v s t ar as n ua sh r) = do
putWord32le v
putWord64le s
putWord64le t
put ar
put as
putWord64le n
put ua
putWord32le sh
putBool r
-- | 0x00 is 'False', anything else is 'True'.
getBool :: Get Bool
getBool = go =<< getWord8
where
go 0 = return False
go _ = return True
putBool :: Bool -> Put
putBool True = putWord8 1
putBool False = putWord8 0
-- | A 'MessageCommand' is included in a 'MessageHeader' in order to identify
-- the type of message present in the payload. This allows the message
-- de-serialization code to know how to decode a particular message payload.
-- Every valid 'Message' constructor has a corresponding 'MessageCommand'
-- constructor.
data MessageCommand
= MCVersion
| MCVerAck
| MCAddr
| MCInv
| MCGetData
| MCNotFound
| MCGetBlocks
| MCGetHeaders
| MCTx
| MCBlock
| MCMerkleBlock
| MCHeaders
| MCGetAddr
| MCFilterLoad
| MCFilterAdd
| MCFilterClear
| MCPing
| MCPong
| MCAlert
| MCMempool
| MCReject
| MCSendHeaders
| MCOther ByteString
deriving (Eq, Generic, NFData)
instance Show MessageCommand where
showsPrec _ = shows . commandToString
instance Read MessageCommand where
readPrec = do
String str <- lexP
return (stringToCommand (cs str))
instance Serialize MessageCommand where
get = go <$> getByteString 12
where
go bs =
let str = unpackCommand bs
in stringToCommand str
put mc = putByteString $ packCommand $ commandToString mc
instance IsString MessageCommand where
fromString str = stringToCommand (cs str)
-- | Read a 'MessageCommand' from its string representation.
stringToCommand :: ByteString -> MessageCommand
stringToCommand str = case str of
"version" -> MCVersion
"verack" -> MCVerAck
"addr" -> MCAddr
"inv" -> MCInv
"getdata" -> MCGetData
"notfound" -> MCNotFound
"getblocks" -> MCGetBlocks
"getheaders" -> MCGetHeaders
"tx" -> MCTx
"block" -> MCBlock
"merkleblock" -> MCMerkleBlock
"headers" -> MCHeaders
"getaddr" -> MCGetAddr
"filterload" -> MCFilterLoad
"filteradd" -> MCFilterAdd
"filterclear" -> MCFilterClear
"ping" -> MCPing
"pong" -> MCPong
"alert" -> MCAlert
"mempool" -> MCMempool
"reject" -> MCReject
"sendheaders" -> MCSendHeaders
_ -> MCOther str
-- | Convert a 'MessageCommand' to its string representation.
commandToString :: MessageCommand -> ByteString
commandToString mc = case mc of
MCVersion -> "version"
MCVerAck -> "verack"
MCAddr -> "addr"
MCInv -> "inv"
MCGetData -> "getdata"
MCNotFound -> "notfound"
MCGetBlocks -> "getblocks"
MCGetHeaders -> "getheaders"
MCTx -> "tx"
MCBlock -> "block"
MCMerkleBlock -> "merkleblock"
MCHeaders -> "headers"
MCGetAddr -> "getaddr"
MCFilterLoad -> "filterload"
MCFilterAdd -> "filteradd"
MCFilterClear -> "filterclear"
MCPing -> "ping"
MCPong -> "pong"
MCAlert -> "alert"
MCMempool -> "mempool"
MCReject -> "reject"
MCSendHeaders -> "sendheaders"
MCOther c -> c
-- | Pack a string 'MessageCommand' so that it is exactly 12-bytes long.
packCommand :: ByteString -> ByteString
packCommand s = B.take 12 $
s `mappend` C.replicate 12 '\NUL'
-- | Undo packing done by 'packCommand'.
unpackCommand :: ByteString -> ByteString
unpackCommand = B.takeWhile (/= 0)
-- | Node offers no services.
nodeNone :: Word64
nodeNone = 0
-- | Services indicate node is a full node that can serve full blocks.
nodeNetwork :: Word64
nodeNetwork = 1
-- | Services indicate node allows to request 'UTXO' set.
nodeGetUTXO :: Word64
nodeGetUTXO = 1 `shiftL` 1
-- | Services indicate node accepts bloom filters.
nodeBloom :: Word64
nodeBloom = 1 `shiftL` 2
-- | Services indicate SegWit-capable node.
nodeWitness :: Word64
nodeWitness = 1 `shiftL` 3
-- | Services indicate Xtreme Thinblocks compatibility.
nodeXThin :: Word64
nodeXThin = 1 `shiftL` 4