packages feed

ppad-bolt9-0.0.1: lib/Lightning/Protocol/BOLT9/Types.hs

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

-- |
-- Module: Lightning.Protocol.BOLT9.Types
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Baseline types for BOLT #9 feature flags.

module Lightning.Protocol.BOLT9.Types (
    -- * Context
    Context(..)
  , isChannelContext
  , channelParity

    -- * Bit indices
  , BitIndex
  , unBitIndex
  , bitIndex

    -- * Required/optional level
  , FeatureLevel(..)

    -- * Required/optional bits
  , RequiredBit
  , unRequiredBit
  , requiredBit
  , requiredFromBitIndex

  , OptionalBit
  , unOptionalBit
  , optionalBit
  , optionalFromBitIndex

    -- * Feature vectors
  , FeatureVector
  , unFeatureVector
  , empty
  , fromByteString
  , set
  , clear
  , member
  ) where

import Control.DeepSeq (NFData)
import qualified Data.Bits as B
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Word (Word8, Word16)
import GHC.Generics (Generic)

-- Context ------------------------------------------------------------------

-- | Presentation context for feature flags.
--
-- Per BOLT #9, features are presented in different message contexts:
--
-- * 'Init' - the @init@ message
-- * 'NodeAnn' - @node_announcement@ messages
-- * 'ChanAnn' - @channel_announcement@ messages (normal)
-- * 'ChanAnnOdd' - @channel_announcement@, always odd (optional)
-- * 'ChanAnnEven' - @channel_announcement@, always even (required)
-- * 'Invoice' - BOLT 11 invoices
-- * 'Blinded' - @allowed_features@ field of a blinded path
-- * 'ChanType' - @channel_type@ field when opening channels
data Context
  = Init        -- ^ I: presented in the @init@ message
  | NodeAnn     -- ^ N: presented in @node_announcement@ messages
  | ChanAnn     -- ^ C: presented in @channel_announcement@ message
  | ChanAnnOdd  -- ^ C-: @channel_announcement@, always odd (optional)
  | ChanAnnEven -- ^ C+: @channel_announcement@, always even (required)
  | Invoice     -- ^ 9: presented in BOLT 11 invoices
  | Blinded     -- ^ B: @allowed_features@ field of a blinded path
  | ChanType    -- ^ T: @channel_type@ field when opening channels
  deriving (Eq, Ord, Show, Generic)

instance NFData Context

-- | Check if a context is a channel announcement context (C, C-, or C+).
isChannelContext :: Context -> Bool
isChannelContext ChanAnn     = True
isChannelContext ChanAnnOdd  = True
isChannelContext ChanAnnEven = True
isChannelContext _           = False
{-# INLINE isChannelContext #-}

-- | For channel contexts with forced parity, return 'Just' the required
-- parity: 'True' for even (C+), 'False' for odd (C-). Returns 'Nothing'
-- for contexts without forced parity.
channelParity :: Context -> Maybe Bool
channelParity ChanAnnOdd  = Just False  -- odd
channelParity ChanAnnEven = Just True   -- even
channelParity _           = Nothing
{-# INLINE channelParity #-}

-- FeatureLevel -------------------------------------------------------------

-- | Whether a feature is set as required or optional.
--
-- Per BOLT #9, each feature has a pair of bits: the even bit indicates
-- required (compulsory) support, the odd bit indicates optional support.
data FeatureLevel
  = Required  -- ^ The feature is required (even bit set)
  | Optional  -- ^ The feature is optional (odd bit set)
  deriving (Eq, Ord, Show, Generic)

instance NFData FeatureLevel

-- BitIndex -----------------------------------------------------------------

-- | A bit index into a feature vector. Bit 0 is the least significant bit.
--
-- Valid range: 0-65535 (sufficient for any practical feature flag).
newtype BitIndex = BitIndex { unBitIndex :: Word16 }
  deriving (Eq, Ord, Show, Generic)

instance NFData BitIndex

-- | Smart constructor for 'BitIndex'. Always succeeds since all Word16
-- values are valid.
bitIndex :: Word16 -> BitIndex
bitIndex = BitIndex
{-# INLINE bitIndex #-}

-- RequiredBit --------------------------------------------------------------

-- | A required (compulsory) feature bit. Required bits are always even.
newtype RequiredBit = RequiredBit { unRequiredBit :: Word16 }
  deriving (Eq, Ord, Show, Generic)

instance NFData RequiredBit

-- | Smart constructor for 'RequiredBit'. Returns 'Nothing' if the bit
--   index is odd.
--
--   >>> requiredBit 16
--   Just (RequiredBit {unRequiredBit = 16})
--   >>> requiredBit 17
--   Nothing
requiredBit :: Word16 -> Maybe RequiredBit
requiredBit !w
  | w B..&. 1 == 0 = Just (RequiredBit w)
  | otherwise      = Nothing
{-# INLINE requiredBit #-}

-- | Convert a 'BitIndex' to a 'RequiredBit'. Returns 'Nothing' if odd.
requiredFromBitIndex :: BitIndex -> Maybe RequiredBit
requiredFromBitIndex (BitIndex w) = requiredBit w
{-# INLINE requiredFromBitIndex #-}

-- OptionalBit --------------------------------------------------------------

-- | An optional feature bit. Optional bits are always odd.
newtype OptionalBit = OptionalBit { unOptionalBit :: Word16 }
  deriving (Eq, Ord, Show, Generic)

instance NFData OptionalBit

-- | Smart constructor for 'OptionalBit'. Returns 'Nothing' if the bit
--   index is even.
--
--   >>> optionalBit 17
--   Just (OptionalBit {unOptionalBit = 17})
--   >>> optionalBit 16
--   Nothing
optionalBit :: Word16 -> Maybe OptionalBit
optionalBit !w
  | w B..&. 1 == 1 = Just (OptionalBit w)
  | otherwise      = Nothing
{-# INLINE optionalBit #-}

-- | Convert a 'BitIndex' to an 'OptionalBit'. Returns 'Nothing' if even.
optionalFromBitIndex :: BitIndex -> Maybe OptionalBit
optionalFromBitIndex (BitIndex w) = optionalBit w
{-# INLINE optionalFromBitIndex #-}

-- FeatureVector ------------------------------------------------------------

-- | A feature vector represented as a strict ByteString.
--
-- The vector is stored in big-endian byte order (most significant byte
-- first), with bits numbered from the least significant bit of the last
-- byte. Bit 0 is at position 0 of the last byte.
newtype FeatureVector = FeatureVector { unFeatureVector :: ByteString }
  deriving (Eq, Ord, Show, Generic)

instance NFData FeatureVector

-- | The empty feature vector (no features set).
--
--   >>> empty
--   FeatureVector {unFeatureVector = ""}
empty :: FeatureVector
empty = FeatureVector BS.empty
{-# INLINE empty #-}

-- | Wrap a ByteString as a FeatureVector.
fromByteString :: ByteString -> FeatureVector
fromByteString = FeatureVector
{-# INLINE fromByteString #-}

-- | Set a bit in the feature vector.
--
--   >>> set (bitIndex 0) empty
--   FeatureVector {unFeatureVector = "\SOH"}
--   >>> set (bitIndex 8) empty
--   FeatureVector {unFeatureVector = "\SOH\NUL"}
set :: BitIndex -> FeatureVector -> FeatureVector
set (BitIndex idx) (FeatureVector bs) =
  let byteIdx    = fromIntegral idx `div` 8
      bitOffset  = fromIntegral idx `mod` 8
      len        = BS.length bs
      -- Number of bytes needed to hold this bit
      needed     = byteIdx + 1
      -- Pad with zeros if necessary (prepend to maintain big-endian)
      bs'        = if needed > len
                   then BS.replicate (needed - len) 0 <> bs
                   else bs
      len'       = BS.length bs'
      -- Index from the end (big-endian: last byte has lowest bits)
      realIdx    = len' - 1 - byteIdx
      oldByte    = BS.index bs' realIdx
      newByte    = oldByte B..|. B.shiftL 1 bitOffset
  in  FeatureVector (updateByteAt realIdx newByte bs')
{-# INLINE set #-}

-- | Clear a bit in the feature vector.
clear :: BitIndex -> FeatureVector -> FeatureVector
clear (BitIndex idx) (FeatureVector bs)
  | BS.null bs = FeatureVector bs
  | otherwise  =
      let byteIdx   = fromIntegral idx `div` 8
          bitOffset = fromIntegral idx `mod` 8
          len       = BS.length bs
      in  if byteIdx >= len
          then FeatureVector bs  -- bit not in range, already clear
          else
            let realIdx = len - 1 - byteIdx
                oldByte = BS.index bs realIdx
                newByte = oldByte B..&. B.complement (B.shiftL 1 bitOffset)
            in  FeatureVector (stripLeadingZeros (updateByteAt realIdx newByte bs))
{-# INLINE clear #-}

-- | Test if a bit is set in the feature vector.
--
--   >>> member (bitIndex 0) (set (bitIndex 0) empty)
--   True
--   >>> member (bitIndex 1) (set (bitIndex 0) empty)
--   False
member :: BitIndex -> FeatureVector -> Bool
member (BitIndex idx) (FeatureVector bs)
  | BS.null bs = False
  | otherwise  =
      let byteIdx   = fromIntegral idx `div` 8
          bitOffset = fromIntegral idx `mod` 8
          len       = BS.length bs
      in  if byteIdx >= len
          then False
          else
            let realIdx = len - 1 - byteIdx
                byte    = BS.index bs realIdx
            in  byte B..&. B.shiftL 1 bitOffset /= 0
{-# INLINE member #-}

-- Internal helpers ---------------------------------------------------------

-- | Update a single byte at the given index.
updateByteAt :: Int -> Word8 -> ByteString -> ByteString
updateByteAt !i !w !bs =
  let (before, after) = BS.splitAt i bs
  in  case BS.uncons after of
        Nothing      -> bs  -- shouldn't happen if i is valid
        Just (_, rest) -> before <> BS.singleton w <> rest
{-# INLINE updateByteAt #-}

-- | Remove leading zero bytes from a ByteString.
stripLeadingZeros :: ByteString -> ByteString
stripLeadingZeros = BS.dropWhile (== 0)
{-# INLINE stripLeadingZeros #-}