bolt9

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

commit 28f6b8e310fe4b5afa76c247b0e6cb10e983c562
parent dbba328c885adeea056c1b7c735dca300bca6510
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 20 Apr 2026 14:54:44 +0800

add validated construction and no-both-bits check

- setFeatureWithDeps: sets a feature and all transitive
  dependencies at the same level
- setFeatureForContext: validates context allowance and
  parity (ChanAnnOdd/ChanAnnEven) before setting
- validateNoBothBits: checks that no feature has both
  required and optional bits set simultaneously

All existing API is unchanged.

Diffstat:
Mlib/Lightning/Protocol/BOLT9.hs | 6++++++
Mlib/Lightning/Protocol/BOLT9/Codec.hs | 25+++++++++++++++++++++++++
Mlib/Lightning/Protocol/BOLT9/Validate.hs | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/lib/Lightning/Protocol/BOLT9.hs b/lib/Lightning/Protocol/BOLT9.hs @@ -99,6 +99,9 @@ module Lightning.Protocol.BOLT9 ( -- * Known features -- | The BOLT #9 feature table and lookup functions. , Feature(..) + , KnownFeature(..) + , knownFeatureByBit + , knownFeatureByName , featureByBit , featureByName , knownFeatures @@ -117,6 +120,7 @@ module Lightning.Protocol.BOLT9 ( -- * Feature operations -- | High-level operations using 'Feature' values. , setFeature + , setFeatureWithDeps , hasFeature , isFeatureSet , listFeatures @@ -126,6 +130,8 @@ module Lightning.Protocol.BOLT9 ( , ValidationError(..) , validateLocal , validateRemote + , setFeatureForContext + , validateNoBothBits , highestSetBit , Validate.setBits ) where diff --git a/lib/Lightning/Protocol/BOLT9/Codec.hs b/lib/Lightning/Protocol/BOLT9/Codec.hs @@ -21,6 +21,7 @@ module Lightning.Protocol.BOLT9.Codec ( -- * Feature operations , setFeature + , setFeatureWithDeps , hasFeature , isFeatureSet , listFeatures @@ -105,6 +106,30 @@ setFeature !f !level = setBit targetBit Optional -> baseBit + 1 {-# INLINE setFeature #-} +-- | Set a feature and all its transitive dependencies. +-- +-- Dependencies are set at the same level as the feature itself. +-- Unknown dependencies (not in the known features table) are +-- silently skipped. +-- +-- >>> import Data.Maybe (fromJust) +-- >>> let mpp = fromJust (featureByName "basic_mpp") +-- >>> let fv = setFeatureWithDeps mpp Optional empty +-- >>> isFeatureSet mpp fv +-- True +-- >>> let Just ps = featureByName "payment_secret" +-- >>> isFeatureSet ps fv -- dependency auto-set +-- True +setFeatureWithDeps + :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector +setFeatureWithDeps !f !level !fv = + let !fv' = setFeature f level fv + in foldr setDep fv' (featureDependencies f) + where + setDep !depName !acc = case featureByName depName of + Nothing -> acc + Just dep -> setFeatureWithDeps dep level acc + -- | Check if a feature is set in the vector. -- -- Returns: diff --git a/lib/Lightning/Protocol/BOLT9/Validate.hs b/lib/Lightning/Protocol/BOLT9/Validate.hs @@ -20,6 +20,10 @@ module Lightning.Protocol.BOLT9.Validate ( -- * Remote validation , validateRemote + -- * Validated construction + , setFeatureForContext + , validateNoBothBits + -- * Helpers , highestSetBit , setBits @@ -31,7 +35,8 @@ 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.Codec + (isFeatureSet, setFeature, testBit) import Lightning.Protocol.BOLT9.Features import Lightning.Protocol.BOLT9.Types @@ -144,6 +149,67 @@ parityErrors !ctx !fv = case channelParity ctx of then InvalidParity bit ctx : acc else acc +-- Validated construction ------------------------------------------------------- + +-- | Set a feature in a vector, validating that the feature is +-- allowed in the given context and has correct parity. +-- +-- Checks: +-- +-- * The feature's context list includes the given context +-- (or is empty, meaning all contexts are allowed) +-- * For 'ChanAnnOdd', only 'Optional' (odd bit) is allowed +-- * For 'ChanAnnEven', only 'Required' (even bit) is allowed +-- +-- >>> import Data.Maybe (fromJust) +-- >>> let pm = fromJust (featureByName "option_payment_metadata") +-- >>> setFeatureForContext Invoice pm Optional empty +-- Right ... +-- >>> setFeatureForContext Init pm Optional empty +-- Left (ContextNotAllowed "option_payment_metadata" Init) +setFeatureForContext + :: Context + -> Feature + -> FeatureLevel + -> FeatureVector + -> Either ValidationError FeatureVector +setFeatureForContext !ctx !f !level !fv + | not (null contexts) + , not (contextAllowed ctx contexts) + = Left (ContextNotAllowed (featureName f) ctx) + | otherwise + = case channelParity ctx of + Just True | level == Optional -> + Left (InvalidParity targetBit ctx) + Just False | level == Required -> + Left (InvalidParity targetBit ctx) + _ -> Right (setFeature f level fv) + where + !contexts = featureContexts f + !baseBit = featureBaseBit f + !targetBit = case level of + Required -> baseBit + Optional -> baseBit + 1 + +-- | Validate that no feature has both its required and optional +-- bits set simultaneously. +-- +-- Returns the input vector unchanged on success. +-- +-- >>> validateNoBothBits empty +-- Right ... +validateNoBothBits + :: FeatureVector -> Either ValidationError FeatureVector +validateNoBothBits !fv = go knownFeatures + where + go [] = Right fv + go (f:fs) = + let !baseBit = featureBaseBit f + in if testBit baseBit fv + && testBit (baseBit + 1) fv + then Left (BothBitsSet baseBit (featureName f)) + else go fs + -- Remote validation ---------------------------------------------------------- -- | Validate a feature vector received from a remote peer.