bolt9

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

commit 78c23f1935dceff739ae99086e579b6bfe1afd00
parent 4d4573133f9c4804c728b55773fa9ce4a04ae1dd
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 16:02:11 +0400

Merge impl/validate: validation engine

Adds lib/Lightning/Protocol/BOLT9/Validate.hs with:
- ValidationError ADT with structured error reasons
- validateLocal: checks we create valid feature vectors
  (no both bits, context restrictions, dependency closure)
- validateRemote: checks received vectors
  (unknown optional ok, unknown required rejected)
- highestSetBit, setBits helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mlib/Lightning/Protocol/BOLT9.hs | 8++++++++
Alib/Lightning/Protocol/BOLT9/Validate.hs | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mppad-bolt9.cabal | 1+
3 files changed, 243 insertions(+), 0 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT9.hs b/lib/Lightning/Protocol/BOLT9.hs @@ -61,8 +61,16 @@ module Lightning.Protocol.BOLT9 ( , setFeature , hasFeature , listFeatures + + -- * Validation + , ValidationError(..) + , validateLocal + , validateRemote + , highestSetBit + , Validate.setBits ) where import Lightning.Protocol.BOLT9.Codec as Codec import Lightning.Protocol.BOLT9.Features import Lightning.Protocol.BOLT9.Types as FV +import Lightning.Protocol.BOLT9.Validate as Validate diff --git a/lib/Lightning/Protocol/BOLT9/Validate.hs b/lib/Lightning/Protocol/BOLT9/Validate.hs @@ -0,0 +1,234 @@ +{-# 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 (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 +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 !baseBit = featureBaseBit f + !contexts = featureContexts f + !isSet = testBit baseBit fv || testBit (baseBit + 1) fv + in if isSet && 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 = + let !baseBit = featureBaseBit f + !isSet = testBit baseBit fv || testBit (baseBit + 1) fv + in if isSet + 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 -> + let !depBase = featureBaseBit dep + in if testBit depBase fv || testBit (depBase + 1) 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 +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 - countLeadingZeros byte + !bitIdx = fromIntegral bytePos * 8 + fromIntegral highBit + in Just bitIdx + + countLeadingZeros :: B.Bits a => a -> Int + countLeadingZeros !b = go' 7 + where + go' !n + | n < 0 = 8 + | B.testBit b n = 7 - n + | otherwise = go' (n - 1) + +-- | 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 diff --git a/ppad-bolt9.cabal b/ppad-bolt9.cabal @@ -28,6 +28,7 @@ library Lightning.Protocol.BOLT9.Codec Lightning.Protocol.BOLT9.Features Lightning.Protocol.BOLT9.Types + Lightning.Protocol.BOLT9.Validate build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13