packages feed

ppad-bolt7-0.1.0: lib/Lightning/Protocol/BOLT7/Types.hs

{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}

-- |
-- Module: Lightning.Protocol.BOLT7.Types
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Field types for BOLT #7 gossip messages.

module Lightning.Protocol.BOLT7.Types (
  -- * Short channel ids
    format_short_channel_id

  -- * channel_update flags
  , MessageFlags(..)
  , Forwarding(..)
  , message_flags
  , message_flags_forwarding
  , has_must_be_one
  , ChannelFlags(..)
  , Direction(..)
  , ChannelStatus(..)
  , channel_flags
  , channel_flags_direction
  , channel_flags_status

  -- * node_announcement fields
  , RgbColor(..)
  , Alias(..)
  , alias
  , un_alias
  , Address(..)
  , IPv4Addr(..)
  , ipv4_addr
  , un_ipv4_addr
  , IPv6Addr(..)
  , ipv6_addr
  , un_ipv6_addr
  , TorV2Addr(..)
  , tor_v2_addr
  , un_tor_v2_addr
  , TorV3Addr(..)
  , tor_v3_addr
  , un_tor_v3_addr
  , Hostname(..)
  , hostname
  , un_hostname
  , is_ascii_hostname
  , usable_addresses

  -- * Query fields
  , QueryFlags(..)
  , QueryFlag(..)
  , query_flags
  , has_query_flag
  , QueryOption(..)
  , QueryOptionFlag(..)
  , query_option
  , has_query_option
  , ChannelUpdateTimestamps(..)
  , ChannelUpdateChecksums(..)
  ) where

import Control.DeepSeq (NFData)
import Data.Bits ((.|.), bit, testBit)
import qualified Data.ByteString as BS
import Data.Word (Word8, Word16, Word32, Word64)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT1
  (ShortChannelId, scid_block_height, scid_tx_index, scid_output_index)

-- short channel ids ----------------------------------------------------------

-- | Render a short channel id in the human-readable format of BOLT #7:
--   block height, transaction index and output index, in decimal,
--   separated by @x@.
--
--   >>> fmap format_short_channel_id (short_channel_id 539268 845 1)
--   Just "539268x845x1"
format_short_channel_id :: ShortChannelId -> String
format_short_channel_id s =
  show (scid_block_height s) ++ "x" ++ show (scid_tx_index s) ++ "x"
    ++ show (scid_output_index s)

-- channel_update flags -------------------------------------------------------

-- | The @message_flags@ byte of a @channel_update@: bit 0 is
--   @must_be_one@, bit 1 is @dont_forward@, and the other bits are
--   unassigned. Every byte is a valid value; unassigned bits are kept so
--   that re-encoding reproduces the signed bytes.
newtype MessageFlags = MessageFlags Word8
  deriving (Eq, Show, Generic)

instance NFData MessageFlags

-- | Whether a @channel_update@ may be relayed to other peers (the
--   @dont_forward@ bit).
data Forwarding
  = Forward      -- ^ @dont_forward@ is 0
  | DontForward  -- ^ @dont_forward@ is 1, as for unannounced channels
  deriving (Eq, Ord, Show, Generic)

instance NFData Forwarding

-- | The t'MessageFlags' an origin node sets: @must_be_one@ set, the given
--   @dont_forward@ bit, and no unassigned bits.
--
--   >>> message_flags DontForward
--   MessageFlags 3
message_flags :: Forwarding -> MessageFlags
message_flags Forward     = MessageFlags 0x01
message_flags DontForward = MessageFlags 0x03
{-# INLINE message_flags #-}

-- | The @dont_forward@ bit of t'MessageFlags'.
--
--   >>> message_flags_forwarding (MessageFlags 0x01)
--   Forward
message_flags_forwarding :: MessageFlags -> Forwarding
message_flags_forwarding (MessageFlags w)
  | testBit w 1 = DontForward
  | otherwise   = Forward
{-# INLINE message_flags_forwarding #-}

-- | Whether the @must_be_one@ bit is set. Senders must set it; receivers
--   ignore it.
--
--   >>> has_must_be_one (MessageFlags 0x00)
--   False
has_must_be_one :: MessageFlags -> Bool
has_must_be_one (MessageFlags w) = testBit w 0
{-# INLINE has_must_be_one #-}

-- | The @channel_flags@ byte of a @channel_update@: bit 0 is
--   @direction@, bit 1 is @disable@, and the other bits are unassigned.
--   Every byte is a valid value; unassigned bits are kept so that
--   re-encoding reproduces the signed bytes.
newtype ChannelFlags = ChannelFlags Word8
  deriving (Eq, Show, Generic)

instance NFData ChannelFlags

-- | The node a @channel_update@ originates from.
data Direction
  = NodeOne  -- ^ @node_id_1@ (@direction@ is 0)
  | NodeTwo  -- ^ @node_id_2@ (@direction@ is 1)
  deriving (Eq, Ord, Show, Generic)

instance NFData Direction

-- | Whether a channel direction is usable.
data ChannelStatus
  = Enabled   -- ^ @disable@ is 0
  | Disabled  -- ^ @disable@ is 1
  deriving (Eq, Ord, Show, Generic)

instance NFData ChannelStatus

-- | The t'ChannelFlags' for the given direction and status, with no
--   unassigned bits set.
--
--   >>> channel_flags NodeTwo Disabled
--   ChannelFlags 3
channel_flags :: Direction -> ChannelStatus -> ChannelFlags
channel_flags d s = ChannelFlags (dir .|. sta)
  where
    dir = case d of
      NodeOne -> 0x00
      NodeTwo -> 0x01
    sta = case s of
      Enabled  -> 0x00
      Disabled -> 0x02
{-# INLINE channel_flags #-}

-- | The @direction@ bit of t'ChannelFlags'.
--
--   >>> channel_flags_direction (ChannelFlags 0x05)
--   NodeTwo
channel_flags_direction :: ChannelFlags -> Direction
channel_flags_direction (ChannelFlags w)
  | testBit w 0 = NodeTwo
  | otherwise   = NodeOne
{-# INLINE channel_flags_direction #-}

-- | The @disable@ bit of t'ChannelFlags'.
--
--   >>> channel_flags_status (ChannelFlags 0x02)
--   Disabled
channel_flags_status :: ChannelFlags -> ChannelStatus
channel_flags_status (ChannelFlags w)
  | testBit w 1 = Disabled
  | otherwise   = Enabled
{-# INLINE channel_flags_status #-}

-- node_announcement fields ---------------------------------------------------

-- | A node's colour: red, green and blue.
data RgbColor = RgbColor
  {-# UNPACK #-} !Word8
  {-# UNPACK #-} !Word8
  {-# UNPACK #-} !Word8
  deriving (Eq, Show, Generic)

instance NFData RgbColor

-- | A 32-byte node alias. Origin nodes should use UTF-8 padded with zero
--   bytes, but receivers must accept any bytes.
newtype Alias = Alias BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData Alias

-- | Construct an t'Alias' from exactly 32 bytes.
--
--   >>> alias "too short"
--   Nothing
alias :: BS.ByteString -> Maybe Alias
alias bs
  | BS.length bs == 32 = Just (Alias bs)
  | otherwise          = Nothing
{-# INLINE alias #-}

-- | The bytes of an t'Alias'.
un_alias :: Alias -> BS.ByteString
un_alias (Alias bs) = bs
{-# INLINE un_alias #-}

-- | A 4-byte IPv4 address.
newtype IPv4Addr = IPv4Addr BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData IPv4Addr

-- | Construct an t'IPv4Addr' from exactly 4 bytes.
--
--   >>> ipv4_addr "\127\NUL\NUL\SOH"
--   Just (IPv4Addr "\DEL\NUL\NUL\SOH")
ipv4_addr :: BS.ByteString -> Maybe IPv4Addr
ipv4_addr bs
  | BS.length bs == 4 = Just (IPv4Addr bs)
  | otherwise         = Nothing
{-# INLINE ipv4_addr #-}

-- | The bytes of an t'IPv4Addr'.
un_ipv4_addr :: IPv4Addr -> BS.ByteString
un_ipv4_addr (IPv4Addr bs) = bs
{-# INLINE un_ipv4_addr #-}

-- | A 16-byte IPv6 address.
newtype IPv6Addr = IPv6Addr BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData IPv6Addr

-- | Construct an t'IPv6Addr' from exactly 16 bytes.
ipv6_addr :: BS.ByteString -> Maybe IPv6Addr
ipv6_addr bs
  | BS.length bs == 16 = Just (IPv6Addr bs)
  | otherwise          = Nothing
{-# INLINE ipv6_addr #-}

-- | The bytes of an t'IPv6Addr'.
un_ipv6_addr :: IPv6Addr -> BS.ByteString
un_ipv6_addr (IPv6Addr bs) = bs
{-# INLINE un_ipv6_addr #-}

-- | The 12 data bytes of a deprecated Tor v2 address descriptor (type
--   3). They are kept, uninterpreted, so that re-encoding reproduces
--   the signed bytes.
newtype TorV2Addr = TorV2Addr BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData TorV2Addr

-- | Construct a t'TorV2Addr' from exactly 12 bytes.
tor_v2_addr :: BS.ByteString -> Maybe TorV2Addr
tor_v2_addr bs
  | BS.length bs == 12 = Just (TorV2Addr bs)
  | otherwise          = Nothing
{-# INLINE tor_v2_addr #-}

-- | The bytes of a t'TorV2Addr'.
un_tor_v2_addr :: TorV2Addr -> BS.ByteString
un_tor_v2_addr (TorV2Addr bs) = bs
{-# INLINE un_tor_v2_addr #-}

-- | A 35-byte Tor v3 onion service address: a 32-byte ed25519 public
--   key, a 2-byte checksum and a 1-byte version.
newtype TorV3Addr = TorV3Addr BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData TorV3Addr

-- | Construct a t'TorV3Addr' from exactly 35 bytes.
tor_v3_addr :: BS.ByteString -> Maybe TorV3Addr
tor_v3_addr bs
  | BS.length bs == 35 = Just (TorV3Addr bs)
  | otherwise          = Nothing
{-# INLINE tor_v3_addr #-}

-- | The bytes of a t'TorV3Addr'.
un_tor_v3_addr :: TorV3Addr -> BS.ByteString
un_tor_v3_addr (TorV3Addr bs) = bs
{-# INLINE un_tor_v3_addr #-}

-- | A DNS hostname of at most 255 bytes (the limit of its length byte).
--
--   BOLT #7 requires hostnames to be ASCII, with other characters
--   Punycode-encoded, but receivers keep whatever bytes they are given;
--   see 'is_ascii_hostname'.
newtype Hostname = Hostname BS.ByteString
  deriving (Eq, Show, Generic)

instance NFData Hostname

-- | Construct a t'Hostname' from at most 255 bytes.
--
--   >>> hostname "ln.example.com"
--   Just (Hostname "ln.example.com")
--   >>> hostname (BS.replicate 256 0x61)
--   Nothing
hostname :: BS.ByteString -> Maybe Hostname
hostname bs
  | BS.length bs <= 255 = Just (Hostname bs)
  | otherwise           = Nothing
{-# INLINE hostname #-}

-- | The bytes of a t'Hostname'.
un_hostname :: Hostname -> BS.ByteString
un_hostname (Hostname bs) = bs
{-# INLINE un_hostname #-}

-- | Whether a t'Hostname' is non-empty and ASCII, as BOLT #7 requires.
--
--   >>> fmap is_ascii_hostname (hostname "caf\233")
--   Just False
is_ascii_hostname :: Hostname -> Bool
is_ascii_hostname (Hostname bs) = not (BS.null bs) && BS.all (< 0x80) bs
{-# INLINE is_ascii_hostname #-}

-- | An address descriptor of a known type, with its port where it has
--   one.
data Address
  = AddrIPv4  !IPv4Addr  {-# UNPACK #-} !Word16  -- ^ type 1
  | AddrIPv6  !IPv6Addr  {-# UNPACK #-} !Word16  -- ^ type 2
  | AddrTorV2 !TorV2Addr                         -- ^ type 3 (deprecated)
  | AddrTorV3 !TorV3Addr {-# UNPACK #-} !Word16  -- ^ type 4
  | AddrDNS   !Hostname  {-# UNPACK #-} !Word16  -- ^ type 5
  deriving (Eq, Show, Generic)

instance NFData Address

-- | The addresses a receiver should connect to, per the BOLT #7 receiver
--   rules: drops Tor v2 descriptors, descriptors with port 0, DNS
--   hostnames that are empty or not ASCII, and every DNS descriptor
--   after the first.
usable_addresses :: [Address] -> [Address]
usable_addresses = go False
  where
    go _ [] = []
    go !dns (a : as) = case a of
      AddrIPv4 _ p  -> port p
      AddrIPv6 _ p  -> port p
      AddrTorV2 _   -> go dns as
      AddrTorV3 _ p -> port p
      AddrDNS h p
        | dns                            -> go dns as
        | p /= 0 && is_ascii_hostname h  -> a : go True as
        | otherwise                      -> go True as
      where
        port p
          | p == 0    = go dns as
          | otherwise = a : go dns as

-- query fields ---------------------------------------------------------------

-- | A @query_flags@ bitfield, sent for each short channel id in a
--   @query_short_channel_ids@. Every value is valid; unassigned bits are
--   kept.
newtype QueryFlags = QueryFlags Word64
  deriving (Eq, Show, Generic)

instance NFData QueryFlags

-- | The assigned bits of t'QueryFlags'.
data QueryFlag
  = WantChannelAnnouncement  -- ^ bit 0
  | WantChannelUpdate1       -- ^ bit 1: the @channel_update@ of node 1
  | WantChannelUpdate2       -- ^ bit 2: the @channel_update@ of node 2
  | WantNodeAnnouncement1    -- ^ bit 3: the @node_announcement@ of node 1
  | WantNodeAnnouncement2    -- ^ bit 4: the @node_announcement@ of node 2
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

instance NFData QueryFlag

-- | The t'QueryFlags' with exactly the given bits set.
--
--   >>> query_flags [WantChannelAnnouncement, WantChannelUpdate2]
--   QueryFlags 5
query_flags :: [QueryFlag] -> QueryFlags
query_flags = QueryFlags . foldr (\f acc -> acc .|. bit (fromEnum f)) 0

-- | Whether the given bit is set.
--
--   >>> has_query_flag WantChannelUpdate1 (QueryFlags 2)
--   True
has_query_flag :: QueryFlag -> QueryFlags -> Bool
has_query_flag f (QueryFlags w) = testBit w (fromEnum f)
{-# INLINE has_query_flag #-}

-- | The @query_option_flags@ bitfield of a @query_channel_range@. Every
--   value is valid; unassigned bits are kept.
newtype QueryOption = QueryOption Word64
  deriving (Eq, Show, Generic)

instance NFData QueryOption

-- | The assigned bits of t'QueryOption'.
data QueryOptionFlag
  = WantTimestamps  -- ^ bit 0
  | WantChecksums   -- ^ bit 1
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

instance NFData QueryOptionFlag

-- | The t'QueryOption' with exactly the given bits set.
--
--   >>> query_option [WantTimestamps, WantChecksums]
--   QueryOption 3
query_option :: [QueryOptionFlag] -> QueryOption
query_option = QueryOption . foldr (\f acc -> acc .|. bit (fromEnum f)) 0

-- | Whether the given bit is set.
--
--   >>> has_query_option WantChecksums (QueryOption 1)
--   False
has_query_option :: QueryOptionFlag -> QueryOption -> Bool
has_query_option f (QueryOption w) = testBit w (fromEnum f)
{-# INLINE has_query_option #-}

-- | The timestamps of a channel's latest @channel_update@s, from
--   @node_id_1@ and @node_id_2@ (0 where there is none).
data ChannelUpdateTimestamps = ChannelUpdateTimestamps
  {-# UNPACK #-} !Word32
  {-# UNPACK #-} !Word32
  deriving (Eq, Show, Generic)

instance NFData ChannelUpdateTimestamps

-- | The checksums of a channel's latest @channel_update@s, from
--   @node_id_1@ and @node_id_2@ (0 where there is none).
data ChannelUpdateChecksums = ChannelUpdateChecksums
  {-# UNPACK #-} !Word32
  {-# UNPACK #-} !Word32
  deriving (Eq, Show, Generic)

instance NFData ChannelUpdateChecksums