bolt9

Lightning feature flags, per BOLT #9 (docs.ppad.tech/bolt9).
git clone git://git.ppad.tech/bolt9.git
Log | Files | Refs | README | LICENSE

Validate.hs (8075B)


      1 {-# OPTIONS_HADDOCK prune #-}
      2 {-# LANGUAGE BangPatterns #-}
      3 {-# LANGUAGE DeriveGeneric #-}
      4 
      5 -- |
      6 -- Module: Lightning.Protocol.BOLT9.Validate
      7 -- Copyright: (c) 2025 Jared Tobin
      8 -- License: MIT
      9 -- Maintainer: Jared Tobin <jared@ppad.tech>
     10 --
     11 -- Validation for BOLT #9 feature vectors.
     12 
     13 module Lightning.Protocol.BOLT9.Validate (
     14     -- * Error types
     15     ValidationError(..)
     16 
     17     -- * Local validation
     18   , validateLocal
     19 
     20     -- * Remote validation
     21   , validateRemote
     22 
     23     -- * Helpers
     24   , highestSetBit
     25   , setBits
     26   ) where
     27 
     28 import Control.DeepSeq (NFData)
     29 import Data.ByteString (ByteString)
     30 import qualified Data.ByteString as BS
     31 import qualified Data.Bits as B
     32 import Data.Word (Word16)
     33 import GHC.Generics (Generic)
     34 import Lightning.Protocol.BOLT9.Codec (isFeatureSet, testBit)
     35 import Lightning.Protocol.BOLT9.Features
     36 import Lightning.Protocol.BOLT9.Types
     37 
     38 -- | Validation errors for feature vectors.
     39 data ValidationError
     40   = BothBitsSet {-# UNPACK #-} !Word16 !String
     41     -- ^ Both optional and required bits are set for a feature.
     42     --   Arguments: base bit index, feature name.
     43   | MissingDependency !String !String
     44     -- ^ A feature's dependency is not set.
     45     --   Arguments: feature name, missing dependency name.
     46   | ContextNotAllowed !String !Context
     47     -- ^ A feature is not allowed in the given context.
     48     --   Arguments: feature name, context.
     49   | UnknownRequiredBit {-# UNPACK #-} !Word16
     50     -- ^ An unknown required (even) bit is set (remote validation only).
     51     --   Argument: bit index.
     52   | InvalidParity {-# UNPACK #-} !Word16 !Context
     53     -- ^ A bit has invalid parity for a channel context.
     54     --   Arguments: bit index, context (ChanAnnOdd or ChanAnnEven).
     55   deriving (Eq, Show, Generic)
     56 
     57 instance NFData ValidationError
     58 
     59 -- Local validation -----------------------------------------------------------
     60 
     61 -- | Validate a feature vector for local use (vectors we create/send).
     62 --
     63 --   Checks:
     64 --
     65 --   * No feature has both optional and required bits set
     66 --   * All set features are valid for the given context
     67 --   * All dependencies of set features are also set
     68 --   * C- context forces odd bits only, C+ forces even bits only
     69 --
     70 --   >>> import Data.Maybe (fromJust)
     71 --   >>> import Lightning.Protocol.BOLT9.Codec (setFeature)
     72 --   >>> let mpp = fromJust (featureByName "basic_mpp")
     73 --   >>> let ps = fromJust (featureByName "payment_secret")
     74 --   >>> validateLocal Init (setFeature mpp False empty)
     75 --   Left [MissingDependency "basic_mpp" "payment_secret"]
     76 --   >>> validateLocal Init (setFeature mpp False (setFeature ps False empty))
     77 --   Right ()
     78 validateLocal :: Context -> FeatureVector -> Either [ValidationError] ()
     79 validateLocal !ctx !fv =
     80   let errs = bothBitsErrors fv
     81           ++ contextErrors ctx fv
     82           ++ dependencyErrors fv
     83           ++ parityErrors ctx fv
     84   in  if null errs
     85       then Right ()
     86       else Left errs
     87 
     88 -- | Check for features with both bits set.
     89 bothBitsErrors :: FeatureVector -> [ValidationError]
     90 bothBitsErrors !fv = foldr check [] knownFeatures
     91   where
     92     check !f !acc =
     93       let !baseBit = featureBaseBit f
     94       in  if testBit baseBit fv && testBit (baseBit + 1) fv
     95           then BothBitsSet baseBit (featureName f) : acc
     96           else acc
     97 
     98 -- | Check for features not allowed in the given context.
     99 contextErrors :: Context -> FeatureVector -> [ValidationError]
    100 contextErrors !ctx !fv = foldr check [] knownFeatures
    101   where
    102     check !f !acc =
    103       let !contexts = featureContexts f
    104       in  if   isFeatureSet f fv
    105             && not (null contexts)
    106             && not (contextAllowed ctx contexts)
    107           then ContextNotAllowed (featureName f) ctx : acc
    108           else acc
    109 
    110 -- | Check if a context is allowed given a list of allowed contexts.
    111 contextAllowed :: Context -> [Context] -> Bool
    112 contextAllowed !ctx !allowed = ctx `elem` allowed || channelMatch
    113   where
    114     channelMatch = isChannelContext ctx && any isChannelContext allowed
    115 
    116 -- | Check for missing dependencies.
    117 dependencyErrors :: FeatureVector -> [ValidationError]
    118 dependencyErrors !fv = foldr check [] knownFeatures
    119   where
    120     check !f !acc =
    121       if   isFeatureSet f fv
    122       then checkDeps f (featureDependencies f) ++ acc
    123       else acc
    124 
    125     checkDeps !f = foldr (checkOneDep f) []
    126 
    127     checkOneDep !f !depName !acc =
    128       case featureByName depName of
    129         Nothing   -> acc  -- unknown dep, skip
    130         Just !dep ->
    131           if   isFeatureSet dep fv
    132           then acc
    133           else MissingDependency (featureName f) depName : acc
    134 
    135 -- | Check for parity errors in C- and C+ contexts.
    136 parityErrors :: Context -> FeatureVector -> [ValidationError]
    137 parityErrors !ctx !fv = case channelParity ctx of
    138   Nothing       -> []
    139   Just wantEven -> foldr (checkParity wantEven) [] (setBits fv)
    140   where
    141     checkParity !wantEven !bit !acc =
    142       let isEven = bit `mod` 2 == 0
    143       in  if isEven /= wantEven
    144           then InvalidParity bit ctx : acc
    145           else acc
    146 
    147 -- Remote validation ----------------------------------------------------------
    148 
    149 -- | Validate a feature vector received from a remote peer.
    150 --
    151 --   Checks:
    152 --
    153 --   * Unknown odd (optional) bits are acceptable (ignored)
    154 --   * Unknown even (required) bits are errors
    155 --   * If both bits of a pair are set, treat as required (not an error)
    156 --   * Context restrictions still apply for known features
    157 --
    158 --   >>> import Lightning.Protocol.BOLT9.Codec (setBit)
    159 --   >>> validateRemote Init (setBit 999 empty)  -- unknown odd bit: ok
    160 --   Right ()
    161 --   >>> validateRemote Init (setBit 998 empty)  -- unknown even bit: error
    162 --   Left [UnknownRequiredBit 998]
    163 validateRemote :: Context -> FeatureVector -> Either [ValidationError] ()
    164 validateRemote !ctx !fv =
    165   let errs = unknownRequiredErrors fv
    166           ++ contextErrors ctx fv
    167           ++ parityErrors ctx fv
    168   in  if null errs
    169       then Right ()
    170       else Left errs
    171 
    172 -- | Check for unknown required bits.
    173 unknownRequiredErrors :: FeatureVector -> [ValidationError]
    174 unknownRequiredErrors !fv = foldr check [] (setBits fv)
    175   where
    176     check !bit !acc
    177       | bit `mod` 2 == 1 = acc  -- odd bit, optional, ignore
    178       | otherwise = case featureByBit bit of
    179           Just _  -> acc  -- known feature
    180           Nothing -> UnknownRequiredBit bit : acc
    181 
    182 -- Helpers --------------------------------------------------------------------
    183 
    184 -- | Find the highest set bit in a feature vector.
    185 --
    186 --   Returns 'Nothing' if the vector is empty or has no bits set.
    187 highestSetBit :: FeatureVector -> Maybe Word16
    188 highestSetBit !fv =
    189   let !bs = unFeatureVector fv
    190   in  if BS.null bs
    191       then Nothing
    192       else findHighestBit bs
    193 
    194 -- | Find the highest set bit in a non-empty ByteString.
    195 findHighestBit :: ByteString -> Maybe Word16
    196 findHighestBit !bs = go 0
    197   where
    198     !len = BS.length bs
    199 
    200     go !i
    201       | i >= len  = Nothing
    202       | otherwise =
    203           let !byte = BS.index bs i
    204           in  if byte == 0
    205               then go (i + 1)
    206               else
    207                 let !bytePos = len - 1 - i
    208                     !highBit = 7 - B.countLeadingZeros byte
    209                     !bitIdx  = fromIntegral bytePos * 8 + fromIntegral highBit
    210                 in  Just bitIdx
    211 
    212 -- | Collect all set bits in a feature vector.
    213 --
    214 --   Returns a list of bit indices in ascending order.
    215 setBits :: FeatureVector -> [Word16]
    216 setBits !fv =
    217   let !bs  = unFeatureVector fv
    218       !len = BS.length bs
    219   in  collectBits bs len 0 []
    220 
    221 -- | Collect bits from a ByteString into a list.
    222 collectBits :: ByteString -> Int -> Int -> [Word16] -> [Word16]
    223 collectBits !bs !len !i !acc
    224   | i >= len  = acc
    225   | otherwise =
    226       let !byte    = BS.index bs (len - 1 - i)
    227           !baseIdx = fromIntegral i * 8
    228           !acc'    = collectByteBits byte baseIdx acc
    229       in  collectBits bs len (i + 1) acc'
    230 
    231 -- | Collect set bits from a single byte.
    232 collectByteBits :: B.Bits a => a -> Word16 -> [Word16] -> [Word16]
    233 collectByteBits !byte !baseIdx = go 7
    234   where
    235     go !bit !acc
    236       | bit < 0        = acc
    237       | B.testBit byte bit = go (bit - 1) ((baseIdx + fromIntegral bit) : acc)
    238       | otherwise          = go (bit - 1) acc