packages feed

ppad-bolt9-0.1.0: lib/Lightning/Protocol/BOLT9.hs

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

-- |
-- Module: Lightning.Protocol.BOLT9
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Feature flags for the Lightning Network, per
-- [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md).
--
-- A feature vector is a big-endian bit field. Features are assigned
-- pairs of bits: setting the even bit means the feature is required,
-- setting the odd bit means it is optional (/it's ok to be odd/).
--
-- The examples below assume:
--
-- >>> :set -XOverloadedStrings
-- >>> import Lightning.Protocol.BOLT9
--
-- Build a vector, check it before sending it, and render it:
--
-- >>> let fv = set_feature_with_deps OptionZeroconf Optional empty
-- >>> list_features fv
-- [(OptionScidAlias,Optional),(OptionZeroconf,Optional)]
-- >>> validate_local InitContext fv
-- Right ()
-- >>> render fv
-- "\b\128\NUL\NUL\NUL\NUL\NUL"
--
-- Check a vector received from a peer:
--
-- >>> validate_remote InitContext (parse "\DLE\NUL\NUL")
-- Left (UnknownBit 20 :| [])
-- >>> validate_remote InitContext (parse "\STX\NUL\NUL")
-- Right ()

module Lightning.Protocol.BOLT9 (
  -- * Feature vectors
    FeatureVector
  , parse
  , render
  , empty
  , union

  -- * Bits
  , set_bit
  , clear_bit
  , test_bit
  , set_bits

  -- * Known features
  , Feature(..)
  , known_features
  , feature_by_bit
  , feature_bit
  , feature_name
  , feature_contexts
  , feature_dependencies
  , feature_assumed

  -- * Feature operations
  , FeatureLevel(..)
  , set_feature
  , set_feature_with_deps
  , clear_feature
  , test_feature
  , list_features

  -- * Validation
  , Context(..)
  , ValidationError(..)
  , validate_local
  , validate_remote

  -- * Channel types
  , ChannelType(..)
  , BasicChannelType(..)
  , channel_type
  , channel_type_features
  ) where

import Control.DeepSeq (NFData(..))
import qualified Data.Bits as B
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.List (find)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (isNothing)
import Data.Word (Word8)
import GHC.Generics (Generic)

-- feature vectors -----------------------------------------------------------

-- | A feature vector.
--
--   Bit 0 is the least significant bit of the last byte. A vector
--   produced by 'parse' keeps its bytes exactly, so 'render' gives
--   them back unchanged (signatures over gossip messages cover them).
--   Every other operation produces a minimally-encoded vector, i.e.
--   one without leading zero bytes.
--
--   Equality ignores leading zero bytes.
--
--   >>> parse "\NUL\STX" == parse "\STX"
--   True
--   >>> set_bit 1 empty
--   parse "\STX"
newtype FeatureVector = FeatureVector ByteString

instance Eq FeatureVector where
  FeatureVector a == FeatureVector b = strip a == strip b

instance Show FeatureVector where
  showsPrec d (FeatureVector bs) = showParen (d > 10) $
    showString "parse " . showsPrec 11 bs

instance NFData FeatureVector where
  rnf (FeatureVector bs) = rnf bs

-- | Parse a feature vector from its wire bytes. Every byte string is a
--   valid feature vector.
--
--   >>> test_bit 9 (parse "\STX\NUL")
--   True
parse :: ByteString -> FeatureVector
parse = FeatureVector
{-# INLINE parse #-}

-- | The wire bytes of a feature vector: the original bytes of a vector
--   produced by 'parse', and the minimal encoding of any other.
--
--   >>> render (parse "\NUL\STX")
--   "\NUL\STX"
--   >>> render (union (parse "\NUL\STX") empty)
--   "\STX"
render :: FeatureVector -> ByteString
render (FeatureVector bs) = bs
{-# INLINE render #-}

-- | The empty feature vector.
--
--   >>> render empty
--   ""
empty :: FeatureVector
empty = FeatureVector BS.empty

-- | The bitwise OR of two feature vectors.
--
--   BOLT #1 requires the receiver of an @init@ message to combine its
--   @globalfeatures@ and @features@ fields this way.
--
--   >>> set_bits (union (parse "\STX") (parse "\SOH\NUL"))
--   [1,8]
union :: FeatureVector -> FeatureVector -> FeatureVector
union (FeatureVector a) (FeatureVector b) =
  FeatureVector (combine (B..|.) a b)

-- bits ----------------------------------------------------------------------

-- | Set the bit at the given index. Negative indices are ignored.
--
--   >>> render (set_bit 9 empty)
--   "\STX\NUL"
set_bit :: Int -> FeatureVector -> FeatureVector
set_bit !i (FeatureVector bs)
  | i < 0     = FeatureVector (strip bs)
  | otherwise = FeatureVector (combine (B..|.) bs (single i))

-- | Clear the bit at the given index. Negative indices are ignored.
--
--   >>> render (clear_bit 9 (parse "\STX\SOH"))
--   "\SOH"
clear_bit :: Int -> FeatureVector -> FeatureVector
clear_bit !i (FeatureVector bs)
  | i < 0 || i `quot` 8 >= BS.length s = FeatureVector s
  | otherwise = FeatureVector (combine clr s (single i))
  where
    s = strip bs
    clr x y = x B..&. B.complement y

-- | Test the bit at the given index. Negative indices are never set.
--
--   >>> test_bit 9 (parse "\STX\NUL")
--   True
--   >>> test_bit 8 (parse "\STX\NUL")
--   False
test_bit :: Int -> FeatureVector -> Bool
test_bit !i (FeatureVector bs)
  | i < 0 || q >= len = False
  | otherwise         = B.testBit (BS.index bs (len - 1 - q)) r
  where
    len    = BS.length bs
    (q, r) = i `quotRem` 8

-- | The indices of all set bits, in ascending order.
--
--   >>> set_bits (parse "\STX\NUL\SOH")
--   [0,17]
set_bits :: FeatureVector -> [Int]
set_bits (FeatureVector bs) = go 0
  where
    !len = BS.length bs
    go !q
      | q >= len  = []
      | otherwise =
          let !w = BS.index bs (len - 1 - q)
          in  [q * 8 + r | r <- [0 .. 7], B.testBit w r] ++ go (q + 1)

-- Drop leading zero bytes.
strip :: ByteString -> ByteString
strip = BS.dropWhile (== 0)
{-# INLINE strip #-}

-- The minimal encoding of a vector with only bit i set (i >= 0).
single :: Int -> ByteString
single i = BS.cons (B.bit r) (BS.replicate q 0)
  where
    (q, r) = i `quotRem` 8

-- Combine two encodings bytewise, aligned at their last bytes (the
-- shorter one padded with leading zeros), and strip the result.
combine
  :: (Word8 -> Word8 -> Word8) -> ByteString -> ByteString -> ByteString
combine f a b = strip (fst (BS.unfoldrN n step 0))
  where
    !la = BS.length a
    !lb = BS.length b
    !n  = max la lb
    byte s ls k
      | k < n - ls = 0
      | otherwise  = BS.index s (k - n + ls)
    step !k = Just (f (byte a la k) (byte b lb k), k + 1)

-- known features ------------------------------------------------------------

-- | The features assigned in the BOLT #9 table.
data Feature
  = OptionDataLossProtect           -- ^ 0/1 @option_data_loss_protect@
  | OptionUpfrontShutdownScript     -- ^ 4/5 @option_upfront_shutdown_script@
  | GossipQueries                   -- ^ 6/7 @gossip_queries@
  | VarOnionOptin                   -- ^ 8/9 @var_onion_optin@
  | GossipQueriesEx                 -- ^ 10/11 @gossip_queries_ex@
  | OptionStaticRemotekey           -- ^ 12/13 @option_static_remotekey@
  | PaymentSecret                   -- ^ 14/15 @payment_secret@
  | BasicMpp                        -- ^ 16/17 @basic_mpp@
  | OptionSupportLargeChannel       -- ^ 18/19 @option_support_large_channel@
  | OptionAnchors                   -- ^ 22/23 @option_anchors@
  | OptionRouteBlinding             -- ^ 24/25 @option_route_blinding@
  | OptionShutdownAnysegwit         -- ^ 26/27 @option_shutdown_anysegwit@
  | OptionDualFund                  -- ^ 28/29 @option_dual_fund@
  | OptionQuiesce                   -- ^ 34/35 @option_quiesce@
  | OptionAttributionData           -- ^ 36/37 @option_attribution_data@
  | OptionOnionMessages             -- ^ 38/39 @option_onion_messages@
  | ZeroFeeCommitments              -- ^ 40/41 @zero_fee_commitments@
  | OptionProvideStorage            -- ^ 42/43 @option_provide_storage@
  | OptionChannelType               -- ^ 44/45 @option_channel_type@
  | OptionScidAlias                 -- ^ 46/47 @option_scid_alias@
  | OptionPaymentMetadata           -- ^ 48/49 @option_payment_metadata@
  | OptionZeroconf                  -- ^ 50/51 @option_zeroconf@
  | OptionSimpleClose               -- ^ 60/61 @option_simple_close@
  | OptionSplice                    -- ^ 62/63 @option_splice@
  | OptionOnionMessagesOnlyChannels
    -- ^ 66/67 @option_onion_messages_only_channels@
  deriving (Eq, Ord, Show, Generic)

instance NFData Feature

-- | All known features, in bit order.
--
--   >>> length known_features
--   25
known_features :: [Feature]
known_features = [
    OptionDataLossProtect
  , OptionUpfrontShutdownScript
  , GossipQueries
  , VarOnionOptin
  , GossipQueriesEx
  , OptionStaticRemotekey
  , PaymentSecret
  , BasicMpp
  , OptionSupportLargeChannel
  , OptionAnchors
  , OptionRouteBlinding
  , OptionShutdownAnysegwit
  , OptionDualFund
  , OptionQuiesce
  , OptionAttributionData
  , OptionOnionMessages
  , ZeroFeeCommitments
  , OptionProvideStorage
  , OptionChannelType
  , OptionScidAlias
  , OptionPaymentMetadata
  , OptionZeroconf
  , OptionSimpleClose
  , OptionSplice
  , OptionOnionMessagesOnlyChannels
  ]

-- A row of the BOLT #9 table.
data Entry = Entry {
    e_bit      :: !Int
  , e_name     :: !String
  , e_contexts :: ![Context]
  , e_deps     :: ![Feature]
  , e_assumed  :: !Bool
  }

entry :: Feature -> Entry
entry f = case f of
  OptionDataLossProtect ->
    Entry 0 "option_data_loss_protect" [] [] True
  OptionUpfrontShutdownScript ->
    Entry 4 "option_upfront_shutdown_script" _IN [] False
  GossipQueries ->
    Entry 6 "gossip_queries" [] [] False
  VarOnionOptin ->
    Entry 8 "var_onion_optin" [] [] True
  GossipQueriesEx ->
    Entry 10 "gossip_queries_ex" _IN [] False
  OptionStaticRemotekey ->
    Entry 12 "option_static_remotekey" [] [] True
  PaymentSecret ->
    Entry 14 "payment_secret" [] [] True
  BasicMpp ->
    Entry 16 "basic_mpp" _IN9 [PaymentSecret] False
  OptionSupportLargeChannel ->
    Entry 18 "option_support_large_channel" _IN [] False
  OptionAnchors ->
    Entry 22 "option_anchors" _INT [] False
  OptionRouteBlinding ->
    Entry 24 "option_route_blinding" _IN9 [] False
  OptionShutdownAnysegwit ->
    Entry 26 "option_shutdown_anysegwit" _IN [] False
  OptionDualFund ->
    Entry 28 "option_dual_fund" _IN [] False
  OptionQuiesce ->
    Entry 34 "option_quiesce" _IN [] False
  OptionAttributionData ->
    Entry 36 "option_attribution_data" _IN9 [] False
  OptionOnionMessages ->
    Entry 38 "option_onion_messages" _IN [] False
  ZeroFeeCommitments ->
    Entry 40 "zero_fee_commitments" _IN [OptionChannelType] False
  OptionProvideStorage ->
    Entry 42 "option_provide_storage" _IN [] False
  OptionChannelType ->
    Entry 44 "option_channel_type" [] [] True
  OptionScidAlias ->
    Entry 46 "option_scid_alias" _INT [] False
  OptionPaymentMetadata ->
    Entry 48 "option_payment_metadata" [InvoiceContext] [] False
  OptionZeroconf ->
    Entry 50 "option_zeroconf" _INT [OptionScidAlias] False
  OptionSimpleClose ->
    Entry 60 "option_simple_close" _IN [OptionShutdownAnysegwit] False
  OptionSplice ->
    Entry 62 "option_splice" _IN [] False
  OptionOnionMessagesOnlyChannels ->
    Entry 66 "option_onion_messages_only_channels" _IN
      [OptionOnionMessages] False
  where
    _IN  = [InitContext, NodeContext]
    _IN9 = [InitContext, NodeContext, InvoiceContext]
    _INT = [InitContext, NodeContext, ChannelTypeContext]

-- | The feature that a bit (even or odd) belongs to, if any.
--
--   >>> feature_by_bit 16
--   Just BasicMpp
--   >>> feature_by_bit 17
--   Just BasicMpp
--   >>> feature_by_bit 20
--   Nothing
feature_by_bit :: Int -> Maybe Feature
feature_by_bit !i
  | i < 0 || i > max_known_bit = Nothing
  | otherwise = find ((== base) . feature_bit) known_features
  where
    base = i - i `rem` 2

-- The highest bit assigned to any known feature.
max_known_bit :: Int
max_known_bit = foldr (max . (+ 1) . feature_bit) 0 known_features

-- | A feature's even (required) bit. Its odd (optional) bit is the
--   next one up.
--
--   >>> feature_bit BasicMpp
--   16
feature_bit :: Feature -> Int
feature_bit = e_bit . entry

-- | A feature's name in the BOLT #9 table.
--
--   >>> feature_name BasicMpp
--   "basic_mpp"
feature_name :: Feature -> String
feature_name = e_name . entry

-- | The contexts the BOLT #9 table lists for a feature, in table order.
--   The list is empty for ASSUMED features and for 'GossipQueries'.
--
--   >>> feature_contexts BasicMpp
--   [InitContext,NodeContext,InvoiceContext]
--   >>> feature_contexts PaymentSecret
--   []
feature_contexts :: Feature -> [Context]
feature_contexts = e_contexts . entry

-- | A feature's direct dependencies.
--
--   >>> feature_dependencies OptionZeroconf
--   [OptionScidAlias]
feature_dependencies :: Feature -> [Feature]
feature_dependencies = e_deps . entry

-- | Whether BOLT #9 marks a feature ASSUMED, i.e. supported by all
--   nodes.
--
--   >>> feature_assumed PaymentSecret
--   True
feature_assumed :: Feature -> Bool
feature_assumed = e_assumed . entry

-- feature operations --------------------------------------------------------

-- | The level at which a feature is set.
data FeatureLevel
  = Required  -- ^ the even bit is set
  | Optional  -- ^ the odd bit is set
  deriving (Eq, Ord, Show, Generic)

instance NFData FeatureLevel

level_bit :: Feature -> FeatureLevel -> Int
level_bit f Required = feature_bit f
level_bit f Optional = feature_bit f + 1
{-# INLINE level_bit #-}

-- | Set a feature at the given level, clearing the other bit of its
--   pair.
--
--   >>> set_bits (set_feature BasicMpp Optional empty)
--   [17]
--   >>> set_bits (set_feature BasicMpp Required (set_bit 17 empty))
--   [16]
set_feature :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector
set_feature f l = set_bit (level_bit f l) . clear_bit (level_bit f other)
  where
    other = case l of
      Required -> Optional
      Optional -> Required

-- | Set a feature at the given level, together with its transitive
--   dependencies (ASSUMED ones included). Dependencies that are not
--   yet set are set at the same level; those already set keep theirs.
--
--   >>> list_features (set_feature_with_deps BasicMpp Optional empty)
--   [(PaymentSecret,Optional),(BasicMpp,Optional)]
set_feature_with_deps
  :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector
set_feature_with_deps f l fv =
    foldr ensure (set_feature f l fv) (feature_dependencies f)
  where
    ensure d acc =
      let acc' = case test_feature d acc of
            Just _  -> acc
            Nothing -> set_feature d l acc
      in  foldr ensure acc' (feature_dependencies d)

-- | Clear both bits of a feature.
--
--   >>> clear_feature BasicMpp (set_feature BasicMpp Optional empty)
--   parse ""
clear_feature :: Feature -> FeatureVector -> FeatureVector
clear_feature f =
  clear_bit (level_bit f Required) . clear_bit (level_bit f Optional)

-- | The level at which a feature is set, if it is.
--
--   A feature with both bits set is 'Required', as BOLT #9 directs the
--   receiver to treat it.
--
--   >>> test_feature BasicMpp (set_bit 17 empty)
--   Just Optional
--   >>> test_feature BasicMpp (set_bit 16 (set_bit 17 empty))
--   Just Required
--   >>> test_feature BasicMpp empty
--   Nothing
test_feature :: Feature -> FeatureVector -> Maybe FeatureLevel
test_feature f fv
  | test_bit (level_bit f Required) fv = Just Required
  | test_bit (level_bit f Optional) fv = Just Optional
  | otherwise                          = Nothing

-- | The known features set in a vector, in bit order.
--
--   >>> list_features (parse "\SOH\128\NUL")
--   [(PaymentSecret,Optional),(BasicMpp,Required)]
list_features :: FeatureVector -> [(Feature, FeatureLevel)]
list_features fv =
  [(f, l) | f <- known_features, Just l <- [test_feature f fv]]

-- validation ----------------------------------------------------------------

-- | A field in which feature bits are presented: the values of the
--   Context column of the BOLT #9 table.
--
--   The table's @C-@ and @C+@ are not further contexts. They mark a
--   feature presented in @channel_announcement@ as always optional or
--   always required. No feature at the implemented spec revision is
--   presented in @channel_announcement@ in any form.
data Context
  = InitContext         -- ^ @I@: the @init@ message
  | NodeContext         -- ^ @N@: @node_announcement@
  | ChannelContext      -- ^ @C@: @channel_announcement@
  | InvoiceContext      -- ^ @9@: BOLT #11 invoices
  | BlindedContext      -- ^ @B@: @allowed_features@ of a blinded path
  | ChannelTypeContext  -- ^ @T@: the @channel_type@ field
  deriving (Eq, Ord, Show, Generic)

instance NFData Context

-- | A rule a feature vector breaks.
data ValidationError
  = UnknownBit {-# UNPACK #-} !Int
    -- ^ a bit belonging to no known feature is set
  | BothBitsSet !Feature
    -- ^ both bits of a feature are set
  | ContextNotAllowed !Feature !Context
    -- ^ a feature is set in a context that doesn't present it
  | MissingDependency !Feature !Feature
    -- ^ a set feature (first) lacks a dependency (second)
  | InvalidChannelType
    -- ^ a @channel_type@ is not a type that BOLT #2 defines
  deriving (Eq, Show, Generic)

instance NFData ValidationError

-- | Validate a feature vector that we're about to send in the given
--   context, per the origin requirements of BOLT #9 and BOLT #1.
--
--   For 'ChannelTypeContext', the vector must be a channel type that
--   BOLT #2 defines (see 'channel_type'). For every other context,
--   checks that:
--
--   * no bit outside the known features is set;
--   * no feature has both bits set;
--   * each set feature is presented in the context. A feature with an
--     empty Context column (the ASSUMED features and 'GossipQueries')
--     is accepted in 'InitContext', 'NodeContext' and
--     'InvoiceContext', where deployed nodes still present it;
--   * each set feature's dependencies are set. A dependency on an
--     ASSUMED feature counts as met even when its bits are clear.
--
--   Errors are reported in that order. No known feature is presented
--   in 'ChannelContext' or 'BlindedContext', so only the empty vector
--   passes there (as BOLT #4 requires of @allowed_features@).
--
--   >>> validate_local InitContext (set_feature OptionZeroconf Optional empty)
--   Left (MissingDependency OptionZeroconf OptionScidAlias :| [])
--   >>> validate_local InitContext (set_feature BasicMpp Optional empty)
--   Right ()
--   >>> validate_local InitContext (set_bit 101 empty)
--   Left (UnknownBit 101 :| [])
validate_local
  :: Context -> FeatureVector -> Either (NonEmpty ValidationError) ()
validate_local ctx fv = to_either $ case ctx of
  ChannelTypeContext -> channel_type_errors fv
  _ ->  unknown_bits (const True) fv
     ++ both_bits_errors fv
     ++ context_errors ctx fv
     ++ dependency_errors fv

-- | Validate a feature vector received in the given context, per the
--   receiver requirements for that context:
--
--   * 'InitContext' (BOLT #1) and 'InvoiceContext' (BOLT #11): no
--     unknown even bit is set, and each set feature's dependencies are
--     set (ASSUMED dependencies count as met).
--   * 'NodeContext' and 'ChannelContext' (BOLT #7): no unknown even
--     bit is set. A failure here means the node or channel must not be
--     routed through; the announcement itself is still valid.
--   * 'BlindedContext' (BOLT #4): no unknown bit is set, odd or even.
--   * 'ChannelTypeContext' (BOLT #2): the vector is a defined channel
--     type (see 'channel_type').
--
--   Unknown odd bits are otherwise ignored. A feature with both bits
--   set is not an error; it counts as 'Required' (see 'test_feature').
--   Nor is a known feature outside its listed contexts, since that
--   rule binds only the sender.
--
--   >>> validate_remote InitContext (set_bit 101 empty)
--   Right ()
--   >>> validate_remote InitContext (set_bit 100 empty)
--   Left (UnknownBit 100 :| [])
--   >>> validate_remote BlindedContext (set_bit 101 empty)
--   Left (UnknownBit 101 :| [])
validate_remote
  :: Context -> FeatureVector -> Either (NonEmpty ValidationError) ()
validate_remote ctx fv = to_either $ case ctx of
  InitContext        -> unknown_even ++ dependency_errors fv
  NodeContext        -> unknown_even
  ChannelContext     -> unknown_even
  InvoiceContext     -> unknown_even ++ dependency_errors fv
  BlindedContext     -> unknown_bits (const True) fv
  ChannelTypeContext -> channel_type_errors fv
  where
    unknown_even = unknown_bits even fv

to_either :: [e] -> Either (NonEmpty e) ()
to_either []       = Right ()
to_either (e : es) = Left (e :| es)

-- Unknown set bits satisfying the predicate.
unknown_bits :: (Int -> Bool) -> FeatureVector -> [ValidationError]
unknown_bits p fv =
  [ UnknownBit i
  | i <- set_bits fv, p i, isNothing (feature_by_bit i) ]

both_bits_errors :: FeatureVector -> [ValidationError]
both_bits_errors fv =
  [ BothBitsSet f
  | f <- known_features
  , test_bit (level_bit f Required) fv
  , test_bit (level_bit f Optional) fv ]

context_errors :: Context -> FeatureVector -> [ValidationError]
context_errors ctx fv =
  [ ContextNotAllowed f ctx
  | (f, _) <- list_features fv, not (presented f) ]
  where
    presented f = case feature_contexts f of
      [] -> ctx `elem` [InitContext, NodeContext, InvoiceContext]
      cs -> ctx `elem` cs

dependency_errors :: FeatureVector -> [ValidationError]
dependency_errors fv =
  [ MissingDependency f d
  | (f, _) <- list_features fv
  , d <- feature_dependencies f
  , not (feature_assumed d)
  , isNothing (test_feature d fv) ]

channel_type_errors :: FeatureVector -> [ValidationError]
channel_type_errors fv = case channel_type fv of
  Nothing -> [InvalidChannelType]
  Just _  -> []

-- channel types -------------------------------------------------------------

-- | The basic channel types of BOLT #2.
data BasicChannelType
  = BasicStaticRemotekey
    -- ^ 'OptionStaticRemotekey' (bit 12)
  | BasicAnchors
    -- ^ 'OptionAnchors' and 'OptionStaticRemotekey' (bits 22 and 12)
  | BasicZeroFeeCommitments
    -- ^ 'ZeroFeeCommitments' (bit 40)
  deriving (Eq, Ord, Show, Generic)

instance NFData BasicChannelType

-- | A channel type, per BOLT #2: a basic type plus any of the
--   'OptionScidAlias' and 'OptionZeroconf' variations.
--
--   Channel types are an enumeration that reuses even feature bits;
--   they aren't subject to the dependency or context rules for
--   feature vectors. BOLT #2 also forbids 'OptionScidAlias' in the
--   channel type of a channel to be announced, which needs the
--   @announce_channel@ flag to check.
data ChannelType = ChannelType {
    ct_basic      :: !BasicChannelType
  , ct_scid_alias :: !Bool
  , ct_zeroconf   :: !Bool
  }
  deriving (Eq, Show, Generic)

instance NFData ChannelType

-- | The channel type that a @channel_type@ vector encodes, if it is a
--   defined one. Leading zero bytes are ignored.
--
--   >>> let zeroconf = ChannelType BasicStaticRemotekey False True
--   >>> channel_type (parse "\EOT\NUL\NUL\NUL\NUL\DLE\NUL") == Just zeroconf
--   True
--   >>> channel_type (set_bit 13 empty)
--   Nothing
channel_type :: FeatureVector -> Maybe ChannelType
channel_type fv = find ((== fv) . channel_type_features) channel_types

-- All defined channel types.
channel_types :: [ChannelType]
channel_types =
  [ ChannelType b s z
  | b <- [BasicStaticRemotekey, BasicAnchors, BasicZeroFeeCommitments]
  , s <- [False, True]
  , z <- [False, True] ]

-- | The @channel_type@ vector for a channel type.
--
--   >>> let anchors = ChannelType BasicAnchors False False
--   >>> render (channel_type_features anchors)
--   "@\DLE\NUL"
channel_type_features :: ChannelType -> FeatureVector
channel_type_features (ChannelType b s z) =
    foldr (\f -> set_feature f Required) empty
      (basic b ++ [OptionScidAlias | s] ++ [OptionZeroconf | z])
  where
    basic BasicStaticRemotekey    = [OptionStaticRemotekey]
    basic BasicAnchors            = [OptionAnchors, OptionStaticRemotekey]
    basic BasicZeroFeeCommitments = [ZeroFeeCommitments]