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:
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.