packages feed

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

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

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

module Lightning.Protocol.BOLT9.Validate (
    -- * Error types
    ValidationError(..)

    -- * Local validation
  , validateLocal

    -- * Remote validation
  , validateRemote

    -- * Helpers
  , highestSetBit
  , setBits
  ) where

import Control.DeepSeq (NFData)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.Bits as B
import Data.Word (Word16)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT9.Codec (isFeatureSet, testBit)
import Lightning.Protocol.BOLT9.Features
import Lightning.Protocol.BOLT9.Types

-- | Validation errors for feature vectors.
data ValidationError
  = BothBitsSet {-# UNPACK #-} !Word16 !String
    -- ^ Both optional and required bits are set for a feature.
    --   Arguments: base bit index, feature name.
  | MissingDependency !String !String
    -- ^ A feature's dependency is not set.
    --   Arguments: feature name, missing dependency name.
  | ContextNotAllowed !String !Context
    -- ^ A feature is not allowed in the given context.
    --   Arguments: feature name, context.
  | UnknownRequiredBit {-# UNPACK #-} !Word16
    -- ^ An unknown required (even) bit is set (remote validation only).
    --   Argument: bit index.
  | InvalidParity {-# UNPACK #-} !Word16 !Context
    -- ^ A bit has invalid parity for a channel context.
    --   Arguments: bit index, context (ChanAnnOdd or ChanAnnEven).
  deriving (Eq, Show, Generic)

instance NFData ValidationError

-- Local validation -----------------------------------------------------------

-- | Validate a feature vector for local use (vectors we create/send).
--
--   Checks:
--
--   * No feature has both optional and required bits set
--   * All set features are valid for the given context
--   * All dependencies of set features are also set
--   * C- context forces odd bits only, C+ forces even bits only
--
--   >>> import Data.Maybe (fromJust)
--   >>> import Lightning.Protocol.BOLT9.Codec (setFeature)
--   >>> let mpp = fromJust (featureByName "basic_mpp")
--   >>> let ps = fromJust (featureByName "payment_secret")
--   >>> validateLocal Init (setFeature mpp False empty)
--   Left [MissingDependency "basic_mpp" "payment_secret"]
--   >>> validateLocal Init (setFeature mpp False (setFeature ps False empty))
--   Right ()
validateLocal :: Context -> FeatureVector -> Either [ValidationError] ()
validateLocal !ctx !fv =
  let errs = bothBitsErrors fv
          ++ contextErrors ctx fv
          ++ dependencyErrors fv
          ++ parityErrors ctx fv
  in  if null errs
      then Right ()
      else Left errs

-- | Check for features with both bits set.
bothBitsErrors :: FeatureVector -> [ValidationError]
bothBitsErrors !fv = foldr check [] knownFeatures
  where
    check !f !acc =
      let !baseBit = featureBaseBit f
      in  if testBit baseBit fv && testBit (baseBit + 1) fv
          then BothBitsSet baseBit (featureName f) : acc
          else acc

-- | Check for features not allowed in the given context.
contextErrors :: Context -> FeatureVector -> [ValidationError]
contextErrors !ctx !fv = foldr check [] knownFeatures
  where
    check !f !acc =
      let !contexts = featureContexts f
      in  if   isFeatureSet f fv
            && not (null contexts)
            && not (contextAllowed ctx contexts)
          then ContextNotAllowed (featureName f) ctx : acc
          else acc

-- | Check if a context is allowed given a list of allowed contexts.
contextAllowed :: Context -> [Context] -> Bool
contextAllowed !ctx !allowed = ctx `elem` allowed || channelMatch
  where
    channelMatch = isChannelContext ctx && any isChannelContext allowed

-- | Check for missing dependencies.
dependencyErrors :: FeatureVector -> [ValidationError]
dependencyErrors !fv = foldr check [] knownFeatures
  where
    check !f !acc =
      if   isFeatureSet f fv
      then checkDeps f (featureDependencies f) ++ acc
      else acc

    checkDeps !f = foldr (checkOneDep f) []

    checkOneDep !f !depName !acc =
      case featureByName depName of
        Nothing   -> acc  -- unknown dep, skip
        Just !dep ->
          if   isFeatureSet dep fv
          then acc
          else MissingDependency (featureName f) depName : acc

-- | Check for parity errors in C- and C+ contexts.
parityErrors :: Context -> FeatureVector -> [ValidationError]
parityErrors !ctx !fv = case channelParity ctx of
  Nothing       -> []
  Just wantEven -> foldr (checkParity wantEven) [] (setBits fv)
  where
    checkParity !wantEven !bit !acc =
      let isEven = bit `mod` 2 == 0
      in  if isEven /= wantEven
          then InvalidParity bit ctx : acc
          else acc

-- Remote validation ----------------------------------------------------------

-- | Validate a feature vector received from a remote peer.
--
--   Checks:
--
--   * Unknown odd (optional) bits are acceptable (ignored)
--   * Unknown even (required) bits are errors
--   * If both bits of a pair are set, treat as required (not an error)
--   * Context restrictions still apply for known features
--
--   >>> import Lightning.Protocol.BOLT9.Codec (setBit)
--   >>> validateRemote Init (setBit 999 empty)  -- unknown odd bit: ok
--   Right ()
--   >>> validateRemote Init (setBit 998 empty)  -- unknown even bit: error
--   Left [UnknownRequiredBit 998]
validateRemote :: Context -> FeatureVector -> Either [ValidationError] ()
validateRemote !ctx !fv =
  let errs = unknownRequiredErrors fv
          ++ contextErrors ctx fv
          ++ parityErrors ctx fv
  in  if null errs
      then Right ()
      else Left errs

-- | Check for unknown required bits.
unknownRequiredErrors :: FeatureVector -> [ValidationError]
unknownRequiredErrors !fv = foldr check [] (setBits fv)
  where
    check !bit !acc
      | bit `mod` 2 == 1 = acc  -- odd bit, optional, ignore
      | otherwise = case featureByBit bit of
          Just _  -> acc  -- known feature
          Nothing -> UnknownRequiredBit bit : acc

-- Helpers --------------------------------------------------------------------

-- | Find the highest set bit in a feature vector.
--
--   Returns 'Nothing' if the vector is empty or has no bits set.
highestSetBit :: FeatureVector -> Maybe Word16
highestSetBit !fv =
  let !bs = unFeatureVector fv
  in  if BS.null bs
      then Nothing
      else findHighestBit bs

-- | Find the highest set bit in a non-empty ByteString.
findHighestBit :: ByteString -> Maybe Word16
findHighestBit !bs = go 0
  where
    !len = BS.length bs

    go !i
      | i >= len  = Nothing
      | otherwise =
          let !byte = BS.index bs i
          in  if byte == 0
              then go (i + 1)
              else
                let !bytePos = len - 1 - i
                    !highBit = 7 - B.countLeadingZeros byte
                    !bitIdx  = fromIntegral bytePos * 8 + fromIntegral highBit
                in  Just bitIdx

-- | Collect all set bits in a feature vector.
--
--   Returns a list of bit indices in ascending order.
setBits :: FeatureVector -> [Word16]
setBits !fv =
  let !bs  = unFeatureVector fv
      !len = BS.length bs
  in  collectBits bs len 0 []

-- | Collect bits from a ByteString into a list.
collectBits :: ByteString -> Int -> Int -> [Word16] -> [Word16]
collectBits !bs !len !i !acc
  | i >= len  = acc
  | otherwise =
      let !byte    = BS.index bs (len - 1 - i)
          !baseIdx = fromIntegral i * 8
          !acc'    = collectByteBits byte baseIdx acc
      in  collectBits bs len (i + 1) acc'

-- | Collect set bits from a single byte.
collectByteBits :: B.Bits a => a -> Word16 -> [Word16] -> [Word16]
collectByteBits !byte !baseIdx = go 7
  where
    go !bit !acc
      | bit < 0        = acc
      | B.testBit byte bit = go (bit - 1) ((baseIdx + fromIntegral bit) : acc)
      | otherwise          = go (bit - 1) acc